├── .circleci └── config.yml ├── .firebaserc ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── misc.md ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── coverage ├── badge-branches.svg ├── badge-functions.svg ├── badge-lines.svg └── badge-statements.svg ├── example-site ├── .env ├── .gitignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── agm_16x16.png │ ├── df_10x10.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── font_16.png │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── nc_12x12.png │ ├── robots.txt │ └── typescript.png ├── src │ ├── app.css │ ├── app.tsx │ ├── components │ │ └── navbar.tsx │ ├── examples │ │ ├── example.ts │ │ ├── fov │ │ │ └── basic-fov.ts │ │ ├── general │ │ │ ├── basic-game.ts │ │ │ ├── hello-world-canvas.ts │ │ │ └── hello-world.ts │ │ ├── generation │ │ │ ├── bsp-dungeon.ts │ │ │ ├── cellular.ts │ │ │ └── drunkards-walk.ts │ │ ├── gui │ │ │ ├── bar-widget.ts │ │ │ ├── basic-widget.ts │ │ │ ├── button-widget.ts │ │ │ ├── custom-widget.ts │ │ │ └── label-widget.ts │ │ ├── input │ │ │ ├── mouse-input-font.ts │ │ │ └── mouse-input.ts │ │ └── pathfinding │ │ │ ├── astar.ts │ │ │ ├── dijkstra.ts │ │ │ └── range-finder.ts │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── pages │ │ ├── examples │ │ │ └── index.tsx │ │ └── home.tsx │ ├── react-app-env.d.ts │ ├── reportWebVitals.js │ └── setupTests.js └── tsconfig.json ├── firebase.json ├── globals.mk ├── makefile ├── package-lock.json ├── package.json ├── readme.md ├── rollup.config.ts ├── src ├── calc │ ├── index.ts │ ├── vector.spec.ts │ └── vector.ts ├── fov │ ├── get-ring.spec.ts │ ├── get-ring.ts │ ├── index.ts │ ├── precise.spec.ts │ └── precise.ts ├── generation │ ├── __snapshots__ │ │ └── cellular-automata-builder.spec.ts.snap │ ├── bsp-dungeon-builder.spec.ts │ ├── bsp-dungeon-builder.ts │ ├── builder.spec.ts │ ├── builder.ts │ ├── cellular-automata-builder.spec.ts │ ├── cellular-automata-builder.ts │ ├── drunkards-walk-builder.spec.ts │ ├── drunkards-walk-builder.ts │ ├── index.ts │ ├── util.spec.ts │ └── util.ts ├── gui │ ├── bar-widget.spec.ts │ ├── bar-widget.ts │ ├── button-widget.spec.ts │ ├── button-widget.ts │ ├── container-widget.spec.ts │ ├── container-widget.ts │ ├── index.ts │ ├── label-widget.spec.ts │ ├── label-widget.ts │ ├── panel-widget.spec.ts │ ├── panel-widget.ts │ ├── text-widget.spec.ts │ ├── text-widget.ts │ ├── util │ │ └── draw-borders.ts │ ├── widget.spec.ts │ └── widget.ts ├── input │ ├── index.ts │ ├── keyboard.spec.ts │ ├── keyboard.ts │ ├── keycode.ts │ ├── mouse.spec.ts │ ├── mouse.ts │ └── test-utils.spec.ts ├── malwoden.spec.ts ├── malwoden.ts ├── pathfinding │ ├── astar.spec.ts │ ├── astar.ts │ ├── dijkstra.spec.ts │ ├── dijkstra.ts │ ├── index.ts │ ├── pathfinding-common.ts │ ├── range-finder.spec.ts │ └── range-finder.ts ├── rand │ ├── alea.spec.ts │ ├── alea.ts │ ├── index.ts │ └── rng.ts ├── struct │ ├── index.ts │ ├── line.spec.ts │ ├── line.ts │ ├── priority-queue-array.spec.ts │ ├── priority-queue-array.ts │ ├── priority-queue-heap.spec.ts │ ├── priority-queue-heap.ts │ ├── rect.spec.ts │ ├── rect.ts │ ├── table.spec.ts │ ├── table.ts │ └── vector.ts └── terminal │ ├── __snapshots__ │ ├── char-code.spec.ts.snap │ └── unicodemap.spec.ts.snap │ ├── canvas-terminal.spec.ts │ ├── canvas-terminal.ts │ ├── char-code.spec.ts │ ├── char-code.ts │ ├── color.spec.ts │ ├── color.ts │ ├── display.spec.ts │ ├── display.ts │ ├── glyph.spec.ts │ ├── glyph.ts │ ├── index.ts │ ├── memory-terminal.spec.ts │ ├── memory-terminal.ts │ ├── retro-terminal.spec.ts │ ├── retro-terminal.ts │ ├── terminal.spec.ts │ ├── terminal.ts │ ├── unicodemap.spec.ts │ └── unicodemap.ts └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | version: 2.1 4 | 5 | orbs: 6 | # The Node.js orb contains a set of prepackaged CircleCI configuration you can utilize 7 | # Orbs reduce the amount of configuration required for common tasks. 8 | # See the orb documentation here: https://circleci.com/developer/orbs/orb/circleci/node 9 | node: circleci/node@4.1 10 | 11 | jobs: 12 | test: 13 | docker: 14 | - image: cimg/node:15.1 15 | steps: 16 | - checkout 17 | - node/install-packages 18 | - run: 19 | name: Run tests 20 | command: npm run test_ci 21 | build: 22 | docker: 23 | - image: cimg/node:15.1 24 | steps: 25 | - checkout 26 | - node/install-packages 27 | - run: 28 | name: Run build 29 | command: npm run build 30 | 31 | workflows: 32 | preflight: 33 | jobs: 34 | - test 35 | - build 36 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "malwoden-dev" 4 | }, 5 | "targets": { 6 | "malwoden-dev": { 7 | "hosting": { 8 | "main": [ 9 | "malwoden-dev" 10 | ], 11 | "docs": [ 12 | "malwoden-dev-docs" 13 | ] 14 | } 15 | }, 16 | "malwoden-prod": { 17 | "hosting": { 18 | "main": [ 19 | "malwoden-prod" 20 | ], 21 | "docs": [ 22 | "malwoden-prod-docs" 23 | ] 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/misc.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Misc 3 | about: Misc task for the project, like cleanup or documentation. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Additional context** 11 | Add any other context or screenshots about the feature request here. 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage/** 3 | !coverage/*.svg 4 | .nyc_output 5 | .DS_Store 6 | *.log 7 | .vscode 8 | .idea 9 | dist 10 | compiled 11 | .awcache 12 | .rpt2_cache 13 | docs 14 | .cache 15 | .terraform 16 | .eslintcache 17 | .firebase -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /coverage/badge-branches.svg: -------------------------------------------------------------------------------- 1 | Coverage:branches: 98.84%Coverage:branches98.84% -------------------------------------------------------------------------------- /coverage/badge-functions.svg: -------------------------------------------------------------------------------- 1 | Coverage:functions: 99.29%Coverage:functions99.29% -------------------------------------------------------------------------------- /coverage/badge-lines.svg: -------------------------------------------------------------------------------- 1 | Coverage:lines: 99.83%Coverage:lines99.83% -------------------------------------------------------------------------------- /coverage/badge-statements.svg: -------------------------------------------------------------------------------- 1 | Coverage:statements: 99.79%Coverage:statements99.79% -------------------------------------------------------------------------------- /example-site/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /example-site/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /example-site/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /example-site/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /example-site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-site-3", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.3", 7 | "@material-ui/icons": "^4.11.2", 8 | "@testing-library/jest-dom": "^5.11.9", 9 | "@testing-library/react": "^11.2.3", 10 | "@testing-library/user-event": "^12.6.3", 11 | "fontsource-roboto": "^4.0.0", 12 | "malwoden": "file:..", 13 | "react": "^17.0.1", 14 | "react-ga": "^3.3.0", 15 | "react-scripts": "4.0.1", 16 | "web-vitals": "^0.2.4" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "@types/react": "^17.0.0", 44 | "@types/react-dom": "^17.0.0", 45 | "@types/react-router-dom": "^5.1.7", 46 | "react-dom": "^17.0.1", 47 | "react-router-dom": "^5.2.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example-site/public/agm_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aedalus/malwoden/cd5e054e5ccdf7296b91ee4e61c1ee2f41f9c201/example-site/public/agm_16x16.png -------------------------------------------------------------------------------- /example-site/public/df_10x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aedalus/malwoden/cd5e054e5ccdf7296b91ee4e61c1ee2f41f9c201/example-site/public/df_10x10.png -------------------------------------------------------------------------------- /example-site/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aedalus/malwoden/cd5e054e5ccdf7296b91ee4e61c1ee2f41f9c201/example-site/public/favicon-16x16.png -------------------------------------------------------------------------------- /example-site/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aedalus/malwoden/cd5e054e5ccdf7296b91ee4e61c1ee2f41f9c201/example-site/public/favicon-32x32.png -------------------------------------------------------------------------------- /example-site/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aedalus/malwoden/cd5e054e5ccdf7296b91ee4e61c1ee2f41f9c201/example-site/public/favicon.ico -------------------------------------------------------------------------------- /example-site/public/font_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aedalus/malwoden/cd5e054e5ccdf7296b91ee4e61c1ee2f41f9c201/example-site/public/font_16.png -------------------------------------------------------------------------------- /example-site/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | Malwoden @_, 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /example-site/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aedalus/malwoden/cd5e054e5ccdf7296b91ee4e61c1ee2f41f9c201/example-site/public/logo192.png -------------------------------------------------------------------------------- /example-site/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aedalus/malwoden/cd5e054e5ccdf7296b91ee4e61c1ee2f41f9c201/example-site/public/logo512.png -------------------------------------------------------------------------------- /example-site/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /example-site/public/nc_12x12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aedalus/malwoden/cd5e054e5ccdf7296b91ee4e61c1ee2f41f9c201/example-site/public/nc_12x12.png -------------------------------------------------------------------------------- /example-site/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /example-site/public/typescript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aedalus/malwoden/cd5e054e5ccdf7296b91ee4e61c1ee2f41f9c201/example-site/public/typescript.png -------------------------------------------------------------------------------- /example-site/src/app.css: -------------------------------------------------------------------------------- 1 | #app { 2 | height: 100vh; 3 | } 4 | -------------------------------------------------------------------------------- /example-site/src/app.tsx: -------------------------------------------------------------------------------- 1 | import NavBar from "./components/navbar"; 2 | 3 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; 4 | 5 | import HomePage from "./pages/home"; 6 | import ExamplePage from "./pages/examples"; 7 | 8 | import "./app.css"; 9 | 10 | function App() { 11 | return ( 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | ); 27 | } 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /example-site/src/components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import AppBar from "@material-ui/core/AppBar"; 4 | import Toolbar from "@material-ui/core/Toolbar"; 5 | import Typography from "@material-ui/core/Typography"; 6 | import IconButton from "@material-ui/core/IconButton"; 7 | import Link from "@material-ui/core/Link"; 8 | import Github from "@material-ui/icons/GitHub"; 9 | 10 | import "fontsource-roboto"; 11 | // import "normalize.css"; 12 | 13 | const useStyles = makeStyles((theme) => ({ 14 | appBar: { 15 | borderBottom: `1px solid ${theme.palette.divider}`, 16 | }, 17 | root: { 18 | flexGrow: 1, 19 | }, 20 | menuButton: { 21 | marginRight: theme.spacing(2), 22 | }, 23 | title: {}, 24 | link: { 25 | margin: theme.spacing(1, 1.5), 26 | }, 27 | })); 28 | 29 | export default function ButtonAppBar() { 30 | const classes = useStyles(); 31 | 32 | return ( 33 | 39 | 40 | 41 | 42 | MALWODEN 43 | 44 | 45 | 46 |
47 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /example-site/src/examples/example.ts: -------------------------------------------------------------------------------- 1 | export interface IExample { 2 | loop(): void; 3 | cleanup(): void; 4 | } 5 | -------------------------------------------------------------------------------- /example-site/src/examples/fov/basic-fov.ts: -------------------------------------------------------------------------------- 1 | import { IExample } from "../example"; 2 | import { 3 | Terminal, 4 | Generation, 5 | FOV, 6 | Input, 7 | CharCode, 8 | Color, 9 | Vector2, 10 | Struct, 11 | } from "malwoden"; 12 | 13 | export class BasicFoVExample implements IExample { 14 | mount: HTMLElement; 15 | animRef: number; 16 | terminal: Terminal.RetroTerminal; 17 | fov: FOV.PreciseShadowcasting; 18 | player: Vector2; 19 | explored: Struct.Table; 20 | fov_spaces: { pos: Vector2; r: number; v: number }[] = []; 21 | map: Struct.Table; 22 | 23 | constructor() { 24 | this.mount = document.getElementById("example")!; 25 | this.terminal = new Terminal.RetroTerminal({ 26 | width: 50, 27 | height: 30, 28 | imageURL: "/font_16.png", 29 | charWidth: 16, 30 | charHeight: 16, 31 | mountNode: this.mount, 32 | }); 33 | 34 | this.explored = new Struct.Table(50, 30); 35 | const gen = new Generation.CellularAutomataBuilder({ 36 | width: 50, 37 | height: 30, 38 | wallValue: 1, 39 | floorValue: 0, 40 | }); 41 | gen.randomize(0.65); 42 | gen.doSimulationStep(3); 43 | gen.connect(); 44 | this.map = gen.getMap(); 45 | const free = this.map.filter((_, val) => val === 0); 46 | 47 | this.player = { 48 | x: free[0].x, 49 | y: free[0].y, 50 | }; 51 | 52 | this.fov = new FOV.PreciseShadowcasting({ 53 | lightPasses: (pos) => this.map.get(pos) !== 1, 54 | topology: "eight", 55 | cartesianRange: true, 56 | }); 57 | 58 | // Keyboard 59 | const keyboard = new Input.KeyboardHandler(); 60 | const movement = new Input.KeyboardContext() 61 | .onDown(Input.KeyCode.DownArrow, () => this.attemptMove(0, 1)) 62 | .onDown(Input.KeyCode.LeftArrow, () => this.attemptMove(-1, 0)) 63 | .onDown(Input.KeyCode.RightArrow, () => this.attemptMove(1, 0)) 64 | .onDown(Input.KeyCode.UpArrow, () => this.attemptMove(0, -1)); 65 | 66 | keyboard.setContext(movement); 67 | 68 | this.calcFOV(); 69 | 70 | this.animRef = requestAnimationFrame(() => this.loop()); 71 | } 72 | 73 | calcFOV() { 74 | this.fov_spaces = []; 75 | 76 | this.fov.calculateCallback(this.player, 9.9, (pos, r, v) => { 77 | if (v) { 78 | if (this.explored.isInBounds(pos)) { 79 | this.explored.set(pos, true); 80 | } 81 | this.fov_spaces.push({ pos, r, v }); 82 | } 83 | }); 84 | } 85 | 86 | attemptMove(dx: number, dy: number) { 87 | const x = this.player.x + dx; 88 | const y = this.player.y + dy; 89 | if (this.map.get({ x, y }) !== 1) { 90 | this.player.x = x; 91 | this.player.y = y; 92 | this.calcFOV(); 93 | } 94 | } 95 | 96 | loop() { 97 | this.terminal.clear(); 98 | 99 | // Draw all tiles 100 | for (let x = 0; x < 80; x++) { 101 | for (let y = 0; y < 50; y++) { 102 | if (this.explored.get({ x, y })) { 103 | const isAlive = this.map.get({ x, y }) === 1; 104 | if (isAlive) { 105 | this.terminal.drawCharCode( 106 | { x: x, y: y }, 107 | CharCode.blackSpadeSuit, 108 | Color.DarkGreen.toGrayscale(), 109 | Color.Green.toGrayscale() 110 | ); 111 | } else { 112 | this.terminal.drawCharCode( 113 | { x: x, y: y }, 114 | CharCode.fullBlock, 115 | Color.Green.toGrayscale() 116 | ); 117 | } 118 | } 119 | } 120 | } 121 | 122 | // Draw tiles in fov 123 | for (let { pos, v } of this.fov_spaces) { 124 | const isAlive = this.map.get(pos) === 1; 125 | if (isAlive) { 126 | this.terminal.drawCharCode( 127 | pos, 128 | CharCode.blackSpadeSuit, 129 | Color.DarkGreen.blend(Color.Black, (1 - v) / 2), 130 | Color.Green.blend(Color.Black, (1 - v) / 2) 131 | ); 132 | } else { 133 | this.terminal.drawCharCode( 134 | pos, 135 | CharCode.fullBlock, 136 | Color.Green.blend(Color.Black, (1 - v) / 2) 137 | ); 138 | } 139 | } 140 | 141 | // Draw player 142 | this.terminal.drawCharCode(this.player, CharCode.at, Color.Yellow); 143 | 144 | this.terminal.render(); 145 | 146 | this.animRef = requestAnimationFrame(() => this.loop()); 147 | } 148 | 149 | cleanup() { 150 | window.cancelAnimationFrame(this.animRef); 151 | this.terminal.delete(); 152 | } 153 | } 154 | 155 | export default BasicFoVExample; 156 | -------------------------------------------------------------------------------- /example-site/src/examples/general/hello-world-canvas.ts: -------------------------------------------------------------------------------- 1 | import { Terminal } from "malwoden"; 2 | import { IExample } from "../example"; 3 | 4 | export class HelloWorldCanvasExample implements IExample { 5 | mount: HTMLElement; 6 | animRef: number; 7 | terminal: Terminal.CanvasTerminal; 8 | 9 | constructor() { 10 | this.mount = document.getElementById("example")!; 11 | const font = new Terminal.Font("Courier New", 24); 12 | this.terminal = new Terminal.CanvasTerminal({ 13 | width: 50, 14 | height: 20, 15 | font, 16 | mountNode: this.mount, 17 | }); 18 | 19 | this.animRef = requestAnimationFrame(() => this.loop()); 20 | } 21 | 22 | loop() { 23 | this.terminal.clear(); 24 | this.terminal.writeAt({ x: 1, y: 1 }, "Hello World!"); 25 | 26 | this.terminal.writeAt( 27 | { x: 1, y: 4 }, 28 | "Malwoden can also draw a font to a canvas." 29 | ); 30 | this.terminal.writeAt( 31 | { x: 1, y: 5 }, 32 | "This can help if you don't want to use" 33 | ); 34 | this.terminal.writeAt({ x: 1, y: 6 }, "a CP437 tileset."); 35 | this.terminal.writeAt({ x: 1, y: 18 }, "@_,"); 36 | 37 | this.terminal.render(); 38 | } 39 | 40 | cleanup() { 41 | window.cancelAnimationFrame(this.animRef); 42 | this.terminal.delete(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /example-site/src/examples/general/hello-world.ts: -------------------------------------------------------------------------------- 1 | import { Glyph, Terminal } from "malwoden"; 2 | import { IExample } from "../example"; 3 | 4 | export class HelloWorldExample implements IExample { 5 | mount: HTMLElement; 6 | animRef: number; 7 | terminal: Terminal.RetroTerminal; 8 | 9 | constructor() { 10 | this.mount = document.getElementById("example")!; 11 | this.terminal = new Terminal.RetroTerminal({ 12 | width: 50, 13 | height: 30, 14 | imageURL: "/font_16.png", 15 | charWidth: 16, 16 | charHeight: 16, 17 | mountNode: this.mount, 18 | }); 19 | 20 | this.animRef = requestAnimationFrame(() => this.loop()); 21 | } 22 | 23 | loop() { 24 | this.terminal.clear(); 25 | this.terminal.writeAt({ x: 1, y: 1 }, "Hello World!"); 26 | 27 | this.terminal.writeAt({ x: 1, y: 5 }, "Malwoden is great"); 28 | this.terminal.writeAt({ x: 1, y: 6 }, "at rendering"); 29 | this.terminal.writeAt({ x: 1, y: 7 }, "CP437 tilesets"); 30 | 31 | this.terminal.writeAt({ x: 1, y: 28 }, "@_,"); 32 | 33 | for (let x = 0; x < 16; x++) { 34 | for (let y = 0; y < 16; y++) { 35 | const num = x + y * 16; 36 | this.terminal.drawGlyph( 37 | { x: x + 26, y: y + 5 }, 38 | Glyph.fromCharCode(num) 39 | ); 40 | } 41 | } 42 | this.terminal.render(); 43 | 44 | this.animRef = requestAnimationFrame(() => this.loop()); 45 | } 46 | 47 | cleanup() { 48 | window.cancelAnimationFrame(this.animRef); 49 | this.terminal.delete(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /example-site/src/examples/generation/bsp-dungeon.ts: -------------------------------------------------------------------------------- 1 | import { Terminal, Generation, CharCode, Color, Rand, Struct } from "malwoden"; 2 | import { BSPDungeonNode } from "../../../../dist/types/generation"; 3 | 4 | import { IExample } from "../example"; 5 | 6 | export class BSPDungeonExample implements IExample { 7 | mount: HTMLElement; 8 | animRef: number; 9 | terminal: Terminal.RetroTerminal; 10 | builder: Generation.BSPDungeonBuilder; 11 | rng = new Rand.AleaRNG(Date.now().toString()); 12 | colors: Color[]; 13 | map: Struct.Table; 14 | nodes: BSPDungeonNode[]; 15 | 16 | constructor() { 17 | console.log("foo"); 18 | this.mount = document.getElementById("example")!; 19 | this.terminal = new Terminal.RetroTerminal({ 20 | width: 50, 21 | height: 40, 22 | imageURL: "/font_16.png", 23 | charWidth: 16, 24 | charHeight: 16, 25 | mountNode: this.mount, 26 | }); 27 | 28 | this.builder = new Generation.BSPDungeonBuilder({ 29 | width: 50, 30 | height: 40, 31 | wallTile: 1, 32 | floorTile: 0, 33 | }); 34 | 35 | this.builder.splitByCount(4); 36 | this.builder.createRooms({ minWidth: 3, minHeight: 3, padding: 1 }); 37 | this.builder.createSimpleHallways(); 38 | this.nodes = this.builder.getLeafNodes(); 39 | console.log(this.nodes); 40 | this.map = this.builder.getMap(); 41 | 42 | this.colors = this.builder 43 | .getLeafNodes() 44 | .map( 45 | (_) => 46 | new Color( 47 | this.rng.nextInt(50, 150), 48 | this.rng.nextInt(50, 150), 49 | this.rng.nextInt(50, 150) 50 | ) 51 | ); 52 | 53 | this.animRef = requestAnimationFrame(() => this.loop()); 54 | } 55 | 56 | loop() { 57 | // Draw all the nodes as colored, just for example 58 | for (let i = 0; i < this.nodes.length; i++) { 59 | const area = this.nodes[i]; 60 | const areaColor = this.colors[i]; 61 | 62 | for (let x = area.v1.x; x <= area.v2.x; x++) { 63 | for (let y = area.v1.y; y <= area.v2.y; y++) { 64 | this.terminal.drawCharCode( 65 | { x, y }, 66 | CharCode.blackSpadeSuit, 67 | areaColor 68 | ); 69 | } 70 | } 71 | } 72 | 73 | // Use the map for collisions. Here we can carve out the rooms 74 | // from the general areas. 75 | for (let x = 0; x < this.map.width; x++) { 76 | for (let y = 0; y < this.map.width; y++) { 77 | if (!this.map.get({ x, y })) { 78 | this.terminal.drawCharCode({ x, y }, CharCode.space, Color.Black); 79 | } 80 | } 81 | } 82 | 83 | for (let hallway of this.builder.getHallways()) { 84 | for (let step of hallway) { 85 | this.terminal.drawCharCode(step, CharCode.period, Color.DarkGray); 86 | } 87 | } 88 | 89 | this.terminal.render(); 90 | this.animRef = requestAnimationFrame(() => this.loop()); 91 | } 92 | 93 | cleanup() { 94 | window.cancelAnimationFrame(this.animRef); 95 | this.terminal.delete(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /example-site/src/examples/generation/cellular.ts: -------------------------------------------------------------------------------- 1 | import { Terminal, Generation, CharCode, Color, Struct } from "malwoden"; 2 | import { IExample } from "../example"; 3 | 4 | export class CellularAutomataExample implements IExample { 5 | mount: HTMLElement; 6 | animRef: number; 7 | terminal: Terminal.RetroTerminal; 8 | builder: Generation.CellularAutomataBuilder; 9 | map: Struct.Table; 10 | 11 | constructor() { 12 | this.mount = document.getElementById("example")!; 13 | this.terminal = new Terminal.RetroTerminal({ 14 | width: 50, 15 | height: 30, 16 | imageURL: "/font_16.png", 17 | charWidth: 16, 18 | charHeight: 16, 19 | mountNode: this.mount, 20 | }); 21 | 22 | this.builder = new Generation.CellularAutomataBuilder({ 23 | width: 50, 24 | height: 30, 25 | wallValue: 1, 26 | floorValue: 0, 27 | }); 28 | this.builder.randomize(0.6); 29 | this.builder.doSimulationStep(3); 30 | this.builder.connect(0); 31 | this.map = this.builder.getMap(); 32 | 33 | this.animRef = requestAnimationFrame(() => this.loop()); 34 | } 35 | 36 | loop() { 37 | this.terminal.clear(); 38 | for (let x = 0; x < 80; x++) { 39 | for (let y = 0; y < 50; y++) { 40 | const isAlive = this.map.get({ x: x, y: y }) === 1; 41 | if (isAlive) { 42 | this.terminal.drawCharCode( 43 | { x, y }, 44 | CharCode.blackSpadeSuit, 45 | Color.Green 46 | ); 47 | } 48 | } 49 | } 50 | this.terminal.render(); 51 | 52 | this.animRef = requestAnimationFrame(() => this.loop()); 53 | } 54 | 55 | cleanup() { 56 | window.cancelAnimationFrame(this.animRef); 57 | this.terminal.delete(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /example-site/src/examples/generation/drunkards-walk.ts: -------------------------------------------------------------------------------- 1 | import { Terminal, Generation, CharCode, Color, Struct } from "malwoden"; 2 | import { IExample } from "../example"; 3 | 4 | export class DrunkardsWalkExample implements IExample { 5 | mount: HTMLElement; 6 | animRef: number; 7 | terminal: Terminal.RetroTerminal; 8 | builder: Generation.DrunkardsWalkBuilder; 9 | map: Struct.Table; 10 | 11 | constructor() { 12 | this.mount = document.getElementById("example")!; 13 | this.terminal = new Terminal.RetroTerminal({ 14 | width: 50, 15 | height: 30, 16 | imageURL: "/font_16.png", 17 | charWidth: 16, 18 | charHeight: 16, 19 | mountNode: this.mount, 20 | }); 21 | 22 | this.builder = new Generation.DrunkardsWalkBuilder({ 23 | width: 50, 24 | height: 30, 25 | floorTile: 0, 26 | wallTile: 1, 27 | }); 28 | 29 | this.builder.walk({ 30 | pathCount: 10, 31 | start: { x: 20, y: 20 }, 32 | stepsMin: 10, 33 | stepsMax: 200, 34 | maxCoverage: 0.6, 35 | }); 36 | this.map = this.builder.getMap(); 37 | 38 | this.animRef = requestAnimationFrame(() => this.loop()); 39 | } 40 | 41 | loop() { 42 | this.terminal.clear(); 43 | for (let x = 0; x < this.map.width; x++) { 44 | for (let y = 0; y < this.map.height; y++) { 45 | if (this.map.get({ x, y }) === 0) { 46 | this.terminal.drawCharCode( 47 | { x, y }, 48 | CharCode.blackSquare, 49 | undefined, 50 | Color.RosyBrown 51 | ); 52 | } else { 53 | this.terminal.drawCharCode( 54 | { x, y }, 55 | CharCode.blackUpPointingTriangle, 56 | Color.SaddleBrown, 57 | Color.RosyBrown 58 | ); 59 | } 60 | } 61 | } 62 | 63 | this.animRef = requestAnimationFrame(() => this.loop()); 64 | } 65 | 66 | cleanup() { 67 | window.cancelAnimationFrame(this.animRef); 68 | this.terminal.delete(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /example-site/src/examples/gui/bar-widget.ts: -------------------------------------------------------------------------------- 1 | import { Terminal, GUI, Color, Glyph } from "malwoden"; 2 | import { IExample } from "../example"; 3 | 4 | export class BarWidgetExample implements IExample { 5 | mount: HTMLElement; 6 | animRef: number; 7 | terminal: Terminal.RetroTerminal; 8 | gui: GUI.Widget; 9 | mainPanel: GUI.Widget; 10 | 11 | playerHP = 0; 12 | playerMana = 0; 13 | 14 | constructor() { 15 | this.mount = document.getElementById("example")!; 16 | this.terminal = new Terminal.RetroTerminal({ 17 | width: 50, 18 | height: 30, 19 | imageURL: "/font_16.png", 20 | charWidth: 16, 21 | charHeight: 16, 22 | mountNode: this.mount, 23 | }); 24 | 25 | // Create a Container to hold other widgets! 26 | // We set a terminal at the root of our Widgets 27 | this.gui = new GUI.ContainerWidget().setTerminal(this.terminal); 28 | 29 | this.mainPanel = new GUI.PanelWidget({ 30 | origin: { x: 3, y: 3 }, 31 | initialState: { 32 | width: 40, 33 | height: 20, 34 | borderStyle: "double-bar", 35 | backColor: Color.DimGray, 36 | }, 37 | }).setParent(this.gui); 38 | 39 | new GUI.TextWidget({ 40 | origin: { x: 1, y: 0 }, 41 | initialState: { 42 | text: " Widgets! ", 43 | backColor: Color.DimGray, 44 | }, 45 | }).setParent(this.mainPanel); 46 | 47 | new GUI.TextWidget({ 48 | origin: { x: 2, y: 2 }, 49 | initialState: { 50 | text: "Bars can help represent hp, time, etc", 51 | wrapAt: 25, 52 | backColor: Color.DimGray, 53 | }, 54 | }).setParent(this.mainPanel); 55 | 56 | new GUI.BarWidget({ 57 | origin: { x: 2, y: 5 }, 58 | initialState: { maxValue: 10, width: 10 }, 59 | }) 60 | .setUpdateFunc(() => ({ 61 | currentValue: this.playerHP, 62 | })) 63 | .setParent(this.mainPanel); 64 | 65 | new GUI.BarWidget({ 66 | origin: { x: 2, y: 7 }, 67 | initialState: { 68 | maxValue: 10, 69 | width: 15, 70 | foreGlyph: new Glyph(" ", Color.Red, Color.Red), 71 | backGlyph: new Glyph(" ", Color.DarkRed, Color.DarkRed), 72 | }, 73 | }) 74 | .setUpdateFunc(() => ({ 75 | currentValue: this.playerHP, 76 | })) 77 | .setParent(this.mainPanel); 78 | 79 | new GUI.BarWidget({ 80 | origin: { x: 2, y: 9 }, 81 | initialState: { 82 | maxValue: 10, 83 | width: 20, 84 | foreGlyph: new Glyph("~", Color.Cyan, Color.DarkBlue), 85 | backGlyph: new Glyph(" ", Color.Cyan, Color.DarkBlue), 86 | }, 87 | }) 88 | .setUpdateFunc(() => ({ 89 | currentValue: this.playerHP, 90 | })) 91 | .setParent(this.mainPanel); 92 | 93 | new GUI.BarWidget({ 94 | origin: { x: 2, y: 11 }, 95 | initialState: { 96 | maxValue: 10, 97 | width: 25, 98 | foreGlyph: new Glyph("*", Color.Yellow, Color.DimGray), 99 | backGlyph: new Glyph(" ", Color.Cyan, Color.DimGray), 100 | }, 101 | }) 102 | .setUpdateFunc(() => ({ 103 | currentValue: this.playerHP, 104 | })) 105 | .setParent(this.mainPanel); 106 | 107 | new GUI.TextWidget({ 108 | origin: { x: 2, y: 13 }, 109 | initialState: { 110 | text: "They'll help calculate based on min/max/current values", 111 | wrapAt: 25, 112 | backColor: Color.DimGray, 113 | }, 114 | }).setParent(this.mainPanel); 115 | 116 | // Set up some animations 117 | this.animRef = requestAnimationFrame(() => this.loop()); 118 | } 119 | 120 | loop() { 121 | this.terminal.clear(); 122 | 123 | // Add a simple animation 124 | this.playerHP = Math.abs(Math.sin(Date.now() / 2000) * 10); 125 | this.playerMana = Math.abs(Math.sin(Date.now() / 2000) * 10); 126 | 127 | this.gui.cascadeUpdate(); 128 | this.gui.cascadeDraw(); 129 | this.terminal.render(); 130 | 131 | this.animRef = requestAnimationFrame(() => this.loop()); 132 | } 133 | 134 | cleanup() { 135 | window.cancelAnimationFrame(this.animRef); 136 | this.terminal.delete(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /example-site/src/examples/gui/basic-widget.ts: -------------------------------------------------------------------------------- 1 | import { Terminal, GUI, Color } from "malwoden"; 2 | import { IExample } from "../example"; 3 | 4 | export class BasicWidgetExample implements IExample { 5 | mount: HTMLElement; 6 | animRef: number; 7 | terminal: Terminal.RetroTerminal; 8 | gui: GUI.Widget; 9 | mainPanel: GUI.Widget; 10 | 11 | constructor() { 12 | this.mount = document.getElementById("example")!; 13 | this.terminal = new Terminal.RetroTerminal({ 14 | width: 50, 15 | height: 30, 16 | imageURL: "/font_16.png", 17 | charWidth: 16, 18 | charHeight: 16, 19 | mountNode: this.mount, 20 | }); 21 | 22 | // Create a Container to hold other widgets! 23 | this.gui = new GUI.ContainerWidget().setTerminal(this.terminal); 24 | 25 | this.mainPanel = new GUI.PanelWidget({ 26 | origin: { x: 3, y: 3 }, 27 | initialState: { 28 | width: 40, 29 | height: 20, 30 | borderStyle: "double-bar", 31 | backColor: Color.BlueViolet, 32 | }, 33 | }).setParent(this.gui); 34 | 35 | new GUI.TextWidget({ 36 | origin: { x: 1, y: 0 }, 37 | initialState: { 38 | text: " Widgets! ", 39 | backColor: Color.BlueViolet, 40 | }, 41 | }).setParent(this.mainPanel); 42 | 43 | new GUI.TextWidget({ 44 | origin: { x: 2, y: 2 }, 45 | initialState: { 46 | text: 47 | "Malwoden provides a basic widget framework out of the box. This can help keep elements of your UI together. You can also make custom, reusable components!", 48 | wrapAt: 35, 49 | backColor: Color.BlueViolet, 50 | }, 51 | }).setParent(this.mainPanel); 52 | 53 | // Set up some animations 54 | this.animRef = requestAnimationFrame(() => this.loop()); 55 | } 56 | 57 | loop() { 58 | this.terminal.clear(); 59 | 60 | // Add a simple animation 61 | const curveX = Math.sin(Date.now() / 4000) * 4 + 2; 62 | const curveY = Math.sin(Date.now() / 4000) * 4 + 2; 63 | 64 | this.mainPanel.setOrigin({ 65 | x: 3 + Math.round(curveX), 66 | y: 3 + Math.round(curveY), 67 | }); 68 | 69 | this.gui.cascadeUpdate(); 70 | this.gui.cascadeDraw(); 71 | this.terminal.render(); 72 | 73 | this.animRef = requestAnimationFrame(() => this.loop()); 74 | } 75 | 76 | cleanup() { 77 | window.cancelAnimationFrame(this.animRef); 78 | this.terminal.delete(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /example-site/src/examples/gui/button-widget.ts: -------------------------------------------------------------------------------- 1 | import { Terminal, GUI, Color, Input } from "malwoden"; 2 | import { IExample } from "../example"; 3 | 4 | export class ButtonWidgetExample implements IExample { 5 | mount: HTMLElement; 6 | animRef: number; 7 | terminal: Terminal.RetroTerminal; 8 | gui: GUI.Widget; 9 | mainPanel: GUI.Widget; 10 | mouseContext: Input.MouseContext; 11 | mouseHandler: Input.MouseHandler; 12 | 13 | selectedColor?: Color; 14 | selectedColorName = ""; 15 | 16 | constructor() { 17 | this.mount = document.getElementById("example")!; 18 | this.terminal = new Terminal.RetroTerminal({ 19 | width: 50, 20 | height: 30, 21 | imageURL: "/font_16.png", 22 | charWidth: 16, 23 | charHeight: 16, 24 | mountNode: this.mount, 25 | }); 26 | 27 | this.mouseContext = new Input.MouseContext(); 28 | this.mouseHandler = new Input.MouseHandler().setContext(this.mouseContext); 29 | 30 | // Create a Container to hold other widgets! 31 | // Here we pass a mouse handler as well. 32 | this.gui = new GUI.ContainerWidget() 33 | .setTerminal(this.terminal) 34 | .setMouseHandler(this.mouseHandler) // MouseHandler gives general information even when not clicking 35 | .registerMouseContext(this.mouseContext); // Will listen for mouse clicks 36 | 37 | this.mainPanel = new GUI.PanelWidget({ 38 | origin: { x: 3, y: 3 }, 39 | initialState: { 40 | width: 40, 41 | height: 20, 42 | borderStyle: "double-bar", 43 | backColor: Color.DimGray, 44 | }, 45 | }).setParent(this.gui); 46 | 47 | new GUI.TextWidget({ 48 | origin: { x: 1, y: 0 }, 49 | initialState: { 50 | text: "Buttons! ", 51 | backColor: Color.DimGray, 52 | }, 53 | }).setParent(this.mainPanel); 54 | 55 | new GUI.TextWidget({ 56 | origin: { x: 2, y: 2 }, 57 | initialState: { 58 | text: 59 | "Buttons can detect mouse clicks. A few basic styles are supplied.", 60 | wrapAt: 32, 61 | backColor: Color.DimGray, 62 | }, 63 | }).setParent(this.mainPanel); 64 | 65 | new GUI.ButtonWidget({ 66 | origin: { x: 2, y: 7 }, 67 | initialState: { 68 | text: "Blue!", 69 | backColor: Color.Blue, 70 | hoverColor: Color.LightBlue, 71 | downColor: Color.Blue, 72 | onClick: () => { 73 | this.selectedColor = Color.Blue; 74 | this.selectedColorName = "Blue"; 75 | }, 76 | }, 77 | }).setParent(this.mainPanel); 78 | 79 | new GUI.ButtonWidget({ 80 | origin: { x: 2, y: 9 }, 81 | initialState: { 82 | text: "Green!", 83 | backColor: Color.Green, 84 | hoverColor: Color.LightGreen, 85 | downColor: Color.Green, 86 | padding: 1, 87 | onClick: () => { 88 | this.selectedColor = Color.LightGreen; 89 | this.selectedColorName = "Green"; 90 | }, 91 | }, 92 | }).setParent(this.mainPanel); 93 | 94 | new GUI.ButtonWidget({ 95 | origin: { x: 2, y: 14 }, 96 | initialState: { 97 | text: "Red!", 98 | backColor: Color.Red, 99 | hoverColor: Color.Pink, 100 | downColor: Color.Red, 101 | padding: 1, 102 | borderStyle: "single-bar", 103 | onClick: () => { 104 | this.selectedColor = Color.Red; 105 | this.selectedColorName = "Red"; 106 | }, 107 | }, 108 | }).setParent(this.mainPanel); 109 | 110 | // Set up some animations 111 | this.animRef = requestAnimationFrame(() => this.loop()); 112 | } 113 | 114 | loop() { 115 | this.terminal.clear(); 116 | 117 | this.gui.cascadeUpdate(); 118 | this.gui.cascadeDraw(); 119 | 120 | this.terminal.writeAt( 121 | { x: 20, y: 10 }, 122 | "Pick a color", 123 | Color.White, 124 | Color.DimGray 125 | ); 126 | 127 | if (this.selectedColor) { 128 | this.terminal.writeAt( 129 | { x: 20, y: 12 }, 130 | "You chose ", 131 | Color.White, 132 | Color.DimGray 133 | ); 134 | this.terminal.writeAt( 135 | { x: 30, y: 12 }, 136 | this.selectedColorName, 137 | this.selectedColor, 138 | Color.DimGray 139 | ); 140 | } 141 | this.terminal.render(); 142 | 143 | this.animRef = requestAnimationFrame(() => this.loop()); 144 | } 145 | 146 | cleanup() { 147 | window.cancelAnimationFrame(this.animRef); 148 | this.terminal.delete(); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /example-site/src/examples/gui/custom-widget.ts: -------------------------------------------------------------------------------- 1 | import { Terminal, GUI, Input, Color } from "malwoden"; 2 | import { MouseHandler } from "../../../../dist/types/input"; 3 | import { IExample } from "../example"; 4 | 5 | export class CustomWidgetExample implements IExample { 6 | mount: HTMLElement; 7 | animRef: number; 8 | terminal: Terminal.RetroTerminal; 9 | gui: GUI.Widget; 10 | 11 | mouse: MouseHandler; 12 | 13 | constructor() { 14 | this.mount = document.getElementById("example")!; 15 | this.terminal = new Terminal.RetroTerminal({ 16 | width: 50, 17 | height: 30, 18 | imageURL: "/font_16.png", 19 | charWidth: 16, 20 | charHeight: 16, 21 | mountNode: this.mount, 22 | }); 23 | 24 | this.mouse = new Input.MouseHandler(); 25 | 26 | // Create a Container to hold other widgets! 27 | // Set a terminal at the root of the widgets. 28 | this.gui = new GUI.ContainerWidget().setTerminal(this.terminal); 29 | 30 | new GUI.TextWidget({ 31 | origin: { x: 1, y: 1 }, 32 | initialState: { 33 | text: 34 | "This timer is a custom widget, built in the example on top of the 'Widget' base class.", 35 | wrapAt: 48, 36 | }, 37 | }).setParent(this.gui); 38 | 39 | new GUI.TextWidget({ 40 | origin: { x: 1, y: 4 }, 41 | initialState: { 42 | text: 43 | "Using custom widgets can help to bring your own styles, while providing a flexible framework.", 44 | wrapAt: 48, 45 | }, 46 | }).setParent(this.gui); 47 | 48 | // Completely custom widget, defined at bottom of the file 49 | new TimerWidget({ 50 | origin: { x: 19, y: 15 }, 51 | initialState: { endTime: Date.now() + 1000 * 10 }, // Add 10 seconds to the timer! 52 | }).setParent(this.gui); 53 | 54 | // Set up some animations 55 | this.animRef = requestAnimationFrame(() => this.loop()); 56 | } 57 | 58 | loop() { 59 | this.terminal.clear(); 60 | 61 | this.gui.cascadeUpdate(); 62 | this.gui.cascadeDraw(); 63 | this.terminal.render(); 64 | 65 | this.animRef = requestAnimationFrame(() => this.loop()); 66 | } 67 | 68 | cleanup() { 69 | window.cancelAnimationFrame(this.animRef); 70 | this.terminal.delete(); 71 | } 72 | } 73 | 74 | interface TimerWidgetState { 75 | endTime: number; 76 | } 77 | 78 | class TimerWidget extends GUI.Widget { 79 | getColor(secondsLeft: number) { 80 | if (secondsLeft > 5) return Color.Green; 81 | if (secondsLeft > 1) return Color.Yellow; 82 | return Color.Red; 83 | } 84 | onDraw(): void { 85 | if (!this.terminal) return; // Will get it from a parent class 86 | const currentTime = Date.now(); 87 | const timeLeft = this.state.endTime - currentTime; 88 | const secondsLeft = Math.floor(timeLeft / 1000); 89 | const msLeft = timeLeft % 1000; 90 | 91 | if (secondsLeft > 0 || msLeft > 0) { 92 | this.terminal.writeAt( 93 | this.origin, 94 | `Timer: ${secondsLeft}.${msLeft}`, 95 | this.getColor(secondsLeft) 96 | ); 97 | } else { 98 | this.terminal.writeAt(this.origin, "Time is up!"); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /example-site/src/examples/gui/label-widget.ts: -------------------------------------------------------------------------------- 1 | import { Terminal, GUI, Input, Color } from "malwoden"; 2 | import { MouseHandler } from "../../../../dist/types/input"; 3 | import { IExample } from "../example"; 4 | 5 | export class LabelWidgetExample implements IExample { 6 | mount: HTMLElement; 7 | animRef: number; 8 | terminal: Terminal.RetroTerminal; 9 | gui: GUI.Widget; 10 | labelWidget: GUI.LabelWidget; 11 | 12 | mouse: MouseHandler; 13 | 14 | constructor() { 15 | this.mount = document.getElementById("example")!; 16 | this.terminal = new Terminal.RetroTerminal({ 17 | width: 50, 18 | height: 30, 19 | imageURL: "/font_16.png", 20 | charWidth: 16, 21 | charHeight: 16, 22 | mountNode: this.mount, 23 | }); 24 | 25 | // Create a Container to hold other widgets! 26 | // Set a terminal at the root of the widgets. 27 | this.gui = new GUI.ContainerWidget().setTerminal(this.terminal); 28 | 29 | this.gui.addChild( 30 | new GUI.TextWidget({ 31 | origin: { x: 23, y: 15 }, 32 | initialState: { 33 | text: "Hover!", 34 | }, 35 | }) 36 | ); 37 | 38 | this.labelWidget = new GUI.LabelWidget({ 39 | initialState: { 40 | text: "Hello!", 41 | direction: "right", 42 | backColor: Color.Purple, 43 | }, 44 | }).setParent(this.gui); 45 | 46 | this.mouse = new Input.MouseHandler(); 47 | 48 | this.labelWidget.setUpdateFunc(() => { 49 | const m = this.mouse.getPos(); 50 | const p = this.terminal.windowToTilePoint(m); 51 | this.labelWidget.setOrigin(p); 52 | 53 | return { 54 | text: `(${p.x},${p.y})`, 55 | direction: p.x < this.terminal.width / 2 ? "right" : "left", 56 | }; 57 | }); 58 | 59 | // Set up some animations 60 | this.animRef = requestAnimationFrame(() => this.loop()); 61 | } 62 | 63 | loop() { 64 | this.terminal.clear(); 65 | 66 | this.gui.cascadeUpdate(); 67 | this.gui.cascadeDraw(); 68 | this.terminal.render(); 69 | 70 | this.animRef = requestAnimationFrame(() => this.loop()); 71 | } 72 | 73 | cleanup() { 74 | window.cancelAnimationFrame(this.animRef); 75 | this.terminal.delete(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /example-site/src/examples/input/mouse-input-font.ts: -------------------------------------------------------------------------------- 1 | import { CharCode, Color, Terminal, Input } from "malwoden"; 2 | import { IExample } from "../example"; 3 | 4 | export class MouseInputFontExample implements IExample { 5 | mount: HTMLElement; 6 | animRef: number; 7 | terminal: Terminal.CanvasTerminal; 8 | clickMessage = "Try Clicking!"; 9 | mouse: Input.MouseHandler; 10 | 11 | constructor() { 12 | this.mount = document.getElementById("example")!; 13 | const font = new Terminal.Font("Courier New", 24); 14 | this.terminal = new Terminal.CanvasTerminal({ 15 | width: 50, 16 | height: 20, 17 | font, 18 | mountNode: this.mount, 19 | }); 20 | 21 | this.mouse = new Input.MouseHandler(); 22 | const c = new Input.MouseContext() 23 | .onMouseDown((pos) => { 24 | const termPos = this.terminal.windowToTilePoint(pos); 25 | this.clickMessage = `mousedown on ${termPos.x}, ${termPos.y}`; 26 | }) 27 | .onMouseUp((pos) => { 28 | const termPos = this.terminal.windowToTilePoint(pos); 29 | this.clickMessage = `mouseup on ${termPos.x}, ${termPos.y}`; 30 | }); 31 | this.mouse.setContext(c); 32 | 33 | this.animRef = requestAnimationFrame(() => this.loop()); 34 | } 35 | 36 | loop() { 37 | this.terminal.clear(); 38 | 39 | // Draw mouse position 40 | const mousePos = this.mouse.getPos(); 41 | const char = this.terminal.windowToTilePoint(mousePos); 42 | this.terminal.drawCharCode(char, CharCode.at, Color.Yellow); 43 | 44 | // Draw mouse message 45 | this.terminal.writeAt({ x: 0, y: 0 }, this.clickMessage); 46 | 47 | // Render 48 | this.terminal.render(); 49 | 50 | this.animRef = requestAnimationFrame(() => this.loop()); 51 | } 52 | 53 | cleanup() { 54 | window.cancelAnimationFrame(this.animRef); 55 | this.terminal.delete(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example-site/src/examples/input/mouse-input.ts: -------------------------------------------------------------------------------- 1 | import { CharCode, Color, Terminal, Input } from "malwoden"; 2 | import { IExample } from "../example"; 3 | 4 | export class MouseInputExample implements IExample { 5 | mount: HTMLElement; 6 | animRef: number; 7 | terminal: Terminal.RetroTerminal; 8 | clickMessage = "Try Clicking!"; 9 | mouse: Input.MouseHandler; 10 | 11 | constructor() { 12 | this.mount = document.getElementById("example")!; 13 | this.terminal = new Terminal.RetroTerminal({ 14 | width: 50, 15 | height: 30, 16 | imageURL: "/font_16.png", 17 | charWidth: 16, 18 | charHeight: 16, 19 | mountNode: this.mount, 20 | }); 21 | 22 | this.mouse = new Input.MouseHandler(); 23 | const c = new Input.MouseContext() 24 | .onMouseDown((pos) => { 25 | const termPos = this.terminal.windowToTilePoint(pos); 26 | this.clickMessage = `mousedown on ${termPos.x}, ${termPos.y}`; 27 | }) 28 | .onMouseUp((pos) => { 29 | const termPos = this.terminal.windowToTilePoint(pos); 30 | this.clickMessage = `mouseup on ${termPos.x}, ${termPos.y}`; 31 | }); 32 | this.mouse.setContext(c); 33 | 34 | this.animRef = requestAnimationFrame(() => this.loop()); 35 | } 36 | 37 | loop() { 38 | this.terminal.clear(); 39 | // Draw mouse position 40 | const mousePos = this.mouse.getPos(); 41 | const char = this.terminal.windowToTilePoint(mousePos); 42 | this.terminal.drawCharCode(char, CharCode.at, Color.Yellow); 43 | 44 | // Draw mouse message 45 | this.terminal.writeAt({ x: 0, y: 0 }, this.clickMessage); 46 | 47 | // Render 48 | this.terminal.render(); 49 | 50 | this.animRef = requestAnimationFrame(() => this.loop()); 51 | } 52 | 53 | cleanup() { 54 | window.cancelAnimationFrame(this.animRef); 55 | this.terminal.delete(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example-site/src/examples/pathfinding/astar.ts: -------------------------------------------------------------------------------- 1 | import { IExample } from "../example"; 2 | import { 3 | CharCode, 4 | Color, 5 | Terminal, 6 | Input, 7 | Generation, 8 | Struct, 9 | Rand, 10 | Pathfinding, 11 | Vector2, 12 | } from "malwoden"; 13 | 14 | export class AStarExample implements IExample { 15 | mount: HTMLElement; 16 | animRef: number; 17 | terminal: Terminal.RetroTerminal; 18 | mouse: Input.MouseHandler; 19 | map: Struct.Table; 20 | sandMap: Struct.Table; 21 | player: Vector2; 22 | astar: Pathfinding.AStar; 23 | prevMouse = { x: 0, y: 0 }; 24 | path: Vector2[] | undefined; 25 | 26 | width = 50; 27 | height = 30; 28 | 29 | constructor() { 30 | this.mount = document.getElementById("example")!; 31 | this.terminal = new Terminal.RetroTerminal({ 32 | width: this.width, 33 | height: this.height, 34 | imageURL: "/font_16.png", 35 | charWidth: 16, 36 | charHeight: 16, 37 | mountNode: this.mount, 38 | }); 39 | 40 | this.mouse = new Input.MouseHandler(); 41 | const gen = new Generation.CellularAutomataBuilder({ 42 | width: this.width, 43 | height: this.height, 44 | wallValue: 1, 45 | floorValue: 0, 46 | }); 47 | gen.randomize(); 48 | gen.doSimulationStep(4); 49 | gen.connect(); 50 | this.map = gen.getMap(); 51 | 52 | const sandGen = new Generation.CellularAutomataBuilder({ 53 | width: this.width, 54 | height: this.height, 55 | wallValue: 1, 56 | floorValue: 0, 57 | rng: new Rand.AleaRNG("foo"), 58 | }); 59 | sandGen.randomize(0.65); 60 | sandGen.doSimulationStep(6); 61 | this.sandMap = sandGen.getMap(); 62 | 63 | // Get a random free spot 64 | const freeSpots: Vector2[] = []; 65 | for (let x = 0; x < this.width; x++) { 66 | for (let y = 0; y < this.height; y++) { 67 | const wall = this.map.get({ x, y }); 68 | if (!wall) freeSpots.push({ x, y }); 69 | } 70 | } 71 | 72 | this.player = new Rand.AleaRNG().nextItem(freeSpots)!; 73 | this.astar = new Pathfinding.AStar({ 74 | isBlockedCallback: (pos) => this.map.get(pos) !== 0, 75 | getTerrainCallback: (_, to) => (this.sandMap.get(to) ? 4 : 0.5), 76 | topology: "eight", 77 | }); 78 | 79 | // Get path only when the mouse moves tiles 80 | this.path = this.astar.compute(this.player, { x: 0, y: 0 }); 81 | 82 | this.animRef = requestAnimationFrame(() => this.loop()); 83 | } 84 | 85 | updatePath(newMouse: Vector2) { 86 | if (this.prevMouse.x === newMouse.x && this.prevMouse.y === newMouse.y) 87 | return; 88 | else { 89 | this.path = this.astar.compute(this.player, newMouse); 90 | this.prevMouse = newMouse; 91 | } 92 | } 93 | 94 | loop() { 95 | this.terminal.clear(); 96 | 97 | // Draw Map 98 | for (let x = 0; x < this.width; x++) { 99 | for (let y = 0; y < this.height; y++) { 100 | const isSand = this.sandMap.get({ x, y }); 101 | let background = isSand 102 | ? Color.DarkOliveGreen.blend(Color.Black, 0.6) 103 | : undefined; 104 | const isWall = this.map.get({ x, y }); 105 | let foreground = isWall ? Color.Green : Color.Black; 106 | let charcode = isWall ? CharCode.blackClubSuit : CharCode.blackSquare; 107 | 108 | this.terminal.drawCharCode({ x, y }, charcode, foreground, background); 109 | } 110 | } 111 | 112 | // Draw Mouse 113 | const mousePos = this.mouse.getPos(); 114 | const tilePos = this.terminal.windowToTilePoint(mousePos); 115 | this.terminal.drawCharCode(tilePos, CharCode.asterisk, Color.Cyan); 116 | 117 | this.updatePath(tilePos); 118 | for (let p of this.path ?? []) { 119 | this.terminal.drawCharCode(p, CharCode.asterisk, Color.DarkCyan); 120 | } 121 | 122 | // Draw Player 123 | this.terminal.drawCharCode(this.player, CharCode.at, Color.Yellow); 124 | 125 | // Render Terminal 126 | this.terminal.render(); 127 | 128 | this.animRef = requestAnimationFrame(() => this.loop()); 129 | } 130 | 131 | cleanup() { 132 | window.cancelAnimationFrame(this.animRef); 133 | this.terminal.delete(); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /example-site/src/examples/pathfinding/dijkstra.ts: -------------------------------------------------------------------------------- 1 | import { IExample } from "../example"; 2 | import { 3 | CharCode, 4 | Color, 5 | Terminal, 6 | Input, 7 | Generation, 8 | Struct, 9 | Rand, 10 | Pathfinding, 11 | Vector2, 12 | } from "malwoden"; 13 | 14 | export class DijkstraExample implements IExample { 15 | mount: HTMLElement; 16 | animRef: number; 17 | terminal: Terminal.RetroTerminal; 18 | mouse: Input.MouseHandler; 19 | map: Struct.Table; 20 | sandMap: Struct.Table; 21 | player: Vector2; 22 | dijkstra: Pathfinding.Dijkstra; 23 | prevMouse = { x: 0, y: 0 }; 24 | path: Vector2[] | undefined; 25 | 26 | width = 50; 27 | height = 30; 28 | 29 | constructor() { 30 | this.mount = document.getElementById("example")!; 31 | this.terminal = new Terminal.RetroTerminal({ 32 | width: this.width, 33 | height: this.height, 34 | imageURL: "/font_16.png", 35 | charWidth: 16, 36 | charHeight: 16, 37 | mountNode: this.mount, 38 | }); 39 | 40 | this.mouse = new Input.MouseHandler(); 41 | const gen = new Generation.CellularAutomataBuilder({ 42 | width: this.width, 43 | height: this.height, 44 | wallValue: 1, 45 | floorValue: 0, 46 | }); 47 | gen.randomize(); 48 | gen.doSimulationStep(4); 49 | gen.connect(); 50 | this.map = gen.getMap(); 51 | 52 | const sandGen = new Generation.CellularAutomataBuilder({ 53 | width: this.width, 54 | height: this.height, 55 | wallValue: 1, 56 | floorValue: 0, 57 | rng: new Rand.AleaRNG("foo"), 58 | }); 59 | sandGen.randomize(0.65); 60 | sandGen.doSimulationStep(6); 61 | this.sandMap = sandGen.getMap(); 62 | 63 | // Get a random free spot 64 | const freeSpots: Vector2[] = []; 65 | for (let x = 0; x < this.width; x++) { 66 | for (let y = 0; y < this.height; y++) { 67 | const wall = this.map.get({ x, y }); 68 | if (!wall) freeSpots.push({ x, y }); 69 | } 70 | } 71 | 72 | this.player = new Rand.AleaRNG().nextItem(freeSpots)!; 73 | this.dijkstra = new Pathfinding.Dijkstra({ 74 | isBlockedCallback: (pos) => this.map.get(pos) !== 0, 75 | getTerrainCallback: (_, to) => (this.sandMap.get(to) ? 4 : 0.5), 76 | topology: "eight", 77 | }); 78 | 79 | // Get path only when the mouse moves tiles 80 | this.path = this.dijkstra.compute(this.player, { x: 0, y: 0 }); 81 | 82 | this.animRef = requestAnimationFrame(() => this.loop()); 83 | } 84 | 85 | updatePath(newMouse: Vector2) { 86 | if (this.prevMouse.x === newMouse.x && this.prevMouse.y === newMouse.y) 87 | return; 88 | else { 89 | this.path = this.dijkstra.compute(this.player, newMouse); 90 | this.prevMouse = newMouse; 91 | } 92 | } 93 | 94 | loop() { 95 | this.terminal.clear(); 96 | 97 | // Draw Map 98 | for (let x = 0; x < this.width; x++) { 99 | for (let y = 0; y < this.height; y++) { 100 | const isSand = this.sandMap.get({ x, y }); 101 | let background = isSand 102 | ? Color.DarkOliveGreen.blend(Color.Black, 0.6) 103 | : undefined; 104 | const isWall = this.map.get({ x, y }); 105 | let foreground = isWall ? Color.Green : Color.Black; 106 | let charcode = isWall ? CharCode.blackClubSuit : CharCode.blackSquare; 107 | 108 | this.terminal.drawCharCode({ x, y }, charcode, foreground, background); 109 | } 110 | } 111 | 112 | // Draw Mouse 113 | const mousePos = this.mouse.getPos(); 114 | const tilePos = this.terminal.windowToTilePoint(mousePos); 115 | this.terminal.drawCharCode(tilePos, CharCode.asterisk, Color.Cyan); 116 | 117 | this.updatePath(tilePos); 118 | for (let p of this.path ?? []) { 119 | this.terminal.drawCharCode(p, CharCode.asterisk, Color.DarkCyan); 120 | } 121 | 122 | // Draw Player 123 | this.terminal.drawCharCode(this.player, CharCode.at, Color.Yellow); 124 | 125 | // Render Terminal 126 | this.terminal.render(); 127 | 128 | this.animRef = requestAnimationFrame(() => this.loop()); 129 | } 130 | 131 | cleanup() { 132 | window.cancelAnimationFrame(this.animRef); 133 | this.terminal.delete(); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /example-site/src/examples/pathfinding/range-finder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharCode, 3 | Color, 4 | Terminal, 5 | Input, 6 | Generation, 7 | Struct, 8 | Rand, 9 | Pathfinding, 10 | Vector2, 11 | } from "malwoden"; 12 | import { IExample } from "../example"; 13 | 14 | export class RangeFinderExample implements IExample { 15 | mount: HTMLElement; 16 | animRef: number; 17 | terminal: Terminal.RetroTerminal; 18 | map: Struct.Table; 19 | sandMap: Struct.Table; 20 | player: Vector2; 21 | rangeFinder: Pathfinding.RangeFinder; 22 | 23 | width = 50; 24 | height = 30; 25 | 26 | range: Pathfinding.RangeVector2[]; 27 | 28 | constructor() { 29 | this.mount = document.getElementById("example")!; 30 | this.terminal = new Terminal.RetroTerminal({ 31 | width: this.width, 32 | height: this.height, 33 | imageURL: "/font_16.png", 34 | charWidth: 16, 35 | charHeight: 16, 36 | mountNode: this.mount, 37 | }); 38 | 39 | const gen = new Generation.CellularAutomataBuilder({ 40 | width: this.width, 41 | height: this.height, 42 | wallValue: 1, 43 | floorValue: 0, 44 | }); 45 | gen.randomize(); 46 | gen.doSimulationStep(4); 47 | gen.connect(); 48 | this.map = gen.getMap(); 49 | 50 | const sandGen = new Generation.CellularAutomataBuilder({ 51 | width: this.width, 52 | height: this.height, 53 | wallValue: 1, 54 | floorValue: 0, 55 | rng: new Rand.AleaRNG("foo"), 56 | }); 57 | sandGen.randomize(0.65); 58 | sandGen.doSimulationStep(6); 59 | this.sandMap = sandGen.getMap(); 60 | 61 | // Get a random free spot 62 | const freeSpots: Vector2[] = []; 63 | for (let x = 0; x < this.width; x++) { 64 | for (let y = 0; y < this.height; y++) { 65 | const wall = this.map.get({ x, y }); 66 | if (!wall) freeSpots.push({ x, y }); 67 | } 68 | } 69 | 70 | this.player = new Rand.AleaRNG().nextItem(freeSpots)!; 71 | 72 | this.rangeFinder = new Pathfinding.RangeFinder({ 73 | topology: "eight", 74 | getTerrainCallback: (_, to) => { 75 | if (this.map.get(to)) return 10; 76 | if (this.sandMap.get(to)) return 2; 77 | 78 | return 1; 79 | }, 80 | }); 81 | 82 | this.range = this.rangeFinder.compute({ 83 | start: this.player, 84 | maxRange: 5, 85 | minRange: 1, 86 | }); 87 | 88 | // Keyboard 89 | const keyboard = new Input.KeyboardHandler(); 90 | const movement = new Input.KeyboardContext() 91 | .onDown(Input.KeyCode.DownArrow, () => this.move(0, 1)) 92 | .onDown(Input.KeyCode.LeftArrow, () => this.move(-1, 0)) 93 | .onDown(Input.KeyCode.RightArrow, () => this.move(1, 0)) 94 | .onDown(Input.KeyCode.UpArrow, () => this.move(0, -1)); 95 | 96 | keyboard.setContext(movement); 97 | 98 | this.animRef = requestAnimationFrame(() => this.loop()); 99 | } 100 | 101 | move(dx: number, dy: number) { 102 | const x = this.player.x + dx; 103 | const y = this.player.y + dy; 104 | if (this.map.isInBounds({ x, y }) && this.map.get({ x, y }) === 0) { 105 | this.player.x = x; 106 | this.player.y = y; 107 | } 108 | 109 | // Recompute range on move 110 | // Mark this as dirty as needed 111 | this.range = this.rangeFinder.compute({ 112 | start: this.player, 113 | maxRange: 5, 114 | minRange: 1, 115 | }); 116 | } 117 | 118 | loop() { 119 | this.terminal.clear(); 120 | 121 | for (let x = 0; x < this.width; x++) { 122 | for (let y = 0; y < this.height; y++) { 123 | const isSand = this.sandMap.get({ x, y }); 124 | let background = isSand 125 | ? Color.DarkOliveGreen.blend(Color.Black, 0.6) 126 | : undefined; 127 | const isWall = this.map.get({ x, y }); 128 | let foreground = isWall ? Color.Green : Color.Black; 129 | let charcode = isWall ? CharCode.blackClubSuit : CharCode.blackSquare; 130 | 131 | this.terminal.drawCharCode({ x, y }, charcode, foreground, background); 132 | } 133 | } 134 | 135 | // Draw range 136 | for (let r of this.range) { 137 | this.terminal.drawCharCode(r, CharCode.asterisk, Color.DarkCyan); 138 | } 139 | 140 | // Draw Player 141 | this.terminal.drawCharCode(this.player, CharCode.at, Color.Yellow); 142 | 143 | // Render Terminal 144 | this.terminal.render(); 145 | 146 | this.animRef = requestAnimationFrame(() => this.loop()); 147 | } 148 | 149 | cleanup() { 150 | window.cancelAnimationFrame(this.animRef); 151 | this.terminal.delete(); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /example-site/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /example-site/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./app"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | 7 | import ReactGA from "react-ga"; 8 | 9 | ReactGA.initialize("UA-189817839-1"); 10 | ReactGA.pageview(window.location.pathname + window.location.search); 11 | 12 | ReactDOM.render( 13 | 14 | 15 | , 16 | document.getElementById("root") 17 | ); 18 | 19 | // If you want to start measuring performance in your app, pass a function 20 | // to log results (for example: reportWebVitals(console.log)) 21 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 22 | reportWebVitals(); 23 | -------------------------------------------------------------------------------- /example-site/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example-site/src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Button from "@material-ui/core/Button"; 3 | import Grid from "@material-ui/core/Grid"; 4 | import Typography from "@material-ui/core/Typography"; 5 | import { makeStyles } from "@material-ui/core/styles"; 6 | import Container from "@material-ui/core/Container"; 7 | import GitHubIcon from "@material-ui/icons/GitHub"; 8 | 9 | function Copyright() { 10 | return ( 11 | 12 | {"Free & Open Source | "} 13 | Malwoden {new Date().getFullYear()} 14 | 15 | ); 16 | } 17 | 18 | const useStyles = makeStyles((theme) => ({ 19 | icon: { 20 | marginRight: theme.spacing(2), 21 | }, 22 | heroContent: { 23 | backgroundColor: theme.palette.background.paper, 24 | padding: theme.spacing(8, 0, 6), 25 | }, 26 | heroButtons: { 27 | marginTop: theme.spacing(4), 28 | }, 29 | cardGrid: { 30 | paddingTop: theme.spacing(8), 31 | paddingBottom: theme.spacing(8), 32 | }, 33 | card: { 34 | height: "100%", 35 | display: "flex", 36 | flexDirection: "column", 37 | }, 38 | cardMedia: { 39 | paddingTop: "56.25%", // 16:9 40 | }, 41 | cardContent: { 42 | flexGrow: 1, 43 | }, 44 | footer: { 45 | marginTop: "auto", 46 | backgroundColor: theme.palette.background.paper, 47 | padding: theme.spacing(6), 48 | }, 49 | })); 50 | 51 | export default function Album() { 52 | const classes = useStyles(); 53 | 54 | return ( 55 |
62 |
63 | {/* Hero unit */} 64 |
65 | 66 | 73 | Malwoden 74 | 75 | 81 | An easy to use Roguelike library for Javascript/Typescript. 82 | 83 |
84 | 85 | 86 | 93 | 94 | 95 | 103 | 104 | 105 |
106 |
107 |
108 |
109 | 110 | {/* Footer */} 111 |
112 | 113 |
114 | {/* End footer */} 115 |
116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /example-site/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example-site/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /example-site/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /example-site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": [ 3 | { 4 | "target": "main", 5 | "public": "example-site/build", 6 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 7 | "rewrites": [ 8 | { 9 | "source": "**", 10 | "destination": "/index.html" 11 | } 12 | ] 13 | }, 14 | { 15 | "target": "docs", 16 | "public": "docs", 17 | "rewrites": [ 18 | { 19 | "source": "**", 20 | "destination": "/index.html" 21 | } 22 | ] 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /globals.mk: -------------------------------------------------------------------------------- 1 | .ONESHELL: 2 | .DEFAULT_GOAL := help 3 | .SHELLFLAGS = -ec 4 | 5 | export GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD) 6 | export GIT_HASH := $(shell git rev-parse HEAD) 7 | 8 | export NAMESPACE := $(shell basename $$PWD) 9 | export ARTIFACT_ID := $(NAMESPACE).$(GIT_HASH) 10 | 11 | # --- Formatting -------------------------------------------------------------- 12 | export RED ?= '\033[0;31m' 13 | export GREEN ?= '\033[0;32m' 14 | export NO_COLOR ?= '\033[0m' 15 | 16 | .PHONY : help 17 | help: 18 | @grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 19 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | include ./globals.mk 2 | 3 | ENV := "dev" 4 | 5 | changelog: ## Generates the changelog 6 | npm run changelog 7 | 8 | node_modules: package.json 9 | npm ci 10 | 11 | dist: node_modules 12 | npm run build 13 | 14 | .PHONY: deploy_sites 15 | deploy_sites: example-site/build dist ## Deploy to firebase 16 | firebase -P malwoden-$(ENV) deploy 17 | 18 | example-site/build: 19 | cd example-site 20 | npm ci 21 | npm install .. 22 | npm run build 23 | 24 | tf_init: ## Initializes terraform 25 | cd ./infra/terraform 26 | rm -rf .terraform 27 | terraform init -backend-config="../envs/$(ENV).backend.config" 28 | 29 | tf_plan: ## Plans terraform 30 | cd ./infra/terraform 31 | terraform plan -var-file="../envs/$(ENV).tfvars" 32 | 33 | tf_apply: ## Applies terraform 34 | cd ./infra/terraform 35 | terraform apply -var-file="../envs/$(ENV).tfvars" 36 | 37 | npm_push: dist ## Deploys the library to npm with the next tag 38 | npm publish --tag next 39 | 40 | npm_release: dist ## Adds the latest tag to the current npm pkg 41 | VERSION=$$(node -e "console.log(require('./package.json').version);") 42 | npm dist-tag add malwoden@$$VERSION latest 43 | 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "malwoden", 3 | "version": "0.5.0", 4 | "description": "", 5 | "keywords": [ 6 | "rogue", 7 | "roguelike", 8 | "typescript", 9 | "cp437", 10 | "ascii", 11 | "generation" 12 | ], 13 | "main": "dist/malwoden.umd.js", 14 | "module": "dist/malwoden.es5.js", 15 | "typings": "dist/types/malwoden.d.ts", 16 | "files": [ 17 | "dist" 18 | ], 19 | "author": " ", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/Aedalus/malwoden" 23 | }, 24 | "license": "MIT", 25 | "engines": { 26 | "node": ">=6.0.0" 27 | }, 28 | "scripts": { 29 | "lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'", 30 | "prebuild": "rimraf dist", 31 | "build": "tsc --module commonjs && rollup -c rollup.config.ts && typedoc --out docs --target es6 --mode modules src --readme none --excludePrivate --excludeProtected --excludeExternals", 32 | "start": "rollup -c rollup.config.ts -w", 33 | "test_ci": "jest --coverage src --runInBand", 34 | "test": "jest --coverage src && jest-coverage-badges", 35 | "test:watch": "jest --coverage --watch src", 36 | "test:prod": "npm run lint && npm run test -- --no-cache", 37 | "deploy-docs": "ts-node tools/gh-pages-publish", 38 | "report-coverage": "cat ./coverage/lcov.info | coveralls", 39 | "commit": "git-cz", 40 | "semantic-release": "semantic-release", 41 | "semantic-release-prepare": "ts-node tools/semantic-release-prepare", 42 | "precommit": "lint-staged", 43 | "travis-deploy-once": "travis-deploy-once", 44 | "changelog": "conventional-changelog -i CHANGELOG.md -s" 45 | }, 46 | "lint-staged": { 47 | "{src,test}/**/*.ts": [ 48 | "prettier --write", 49 | "git add" 50 | ] 51 | }, 52 | "config": { 53 | "commitizen": { 54 | "path": "node_modules/cz-conventional-changelog" 55 | } 56 | }, 57 | "jest": { 58 | "globals": { 59 | "ts-jest": { 60 | "tsconfig": { 61 | "target": "es6" 62 | } 63 | } 64 | }, 65 | "transform": { 66 | ".(ts|tsx)": "ts-jest" 67 | }, 68 | "testEnvironment": "node", 69 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 70 | "moduleFileExtensions": [ 71 | "ts", 72 | "tsx", 73 | "js" 74 | ], 75 | "coveragePathIgnorePatterns": [ 76 | "/node_modules/", 77 | "/test/", 78 | "index.ts" 79 | ], 80 | "coverageThreshold": { 81 | "global": { 82 | "branches": 90, 83 | "functions": 95, 84 | "lines": 95, 85 | "statements": 95 86 | } 87 | }, 88 | "collectCoverageFrom": [ 89 | "src/*.{js,ts}", 90 | "src/**.{js,ts}", 91 | "src/**/*.{js,ts}" 92 | ], 93 | "coverageReporters": [ 94 | "json-summary", 95 | "text", 96 | "lcov" 97 | ] 98 | }, 99 | "commitlint": { 100 | "extends": [ 101 | "@commitlint/config-conventional" 102 | ] 103 | }, 104 | "devDependencies": { 105 | "@commitlint/cli": "^7.1.2", 106 | "@commitlint/config-conventional": "^7.1.2", 107 | "@types/jest": "^23.3.2", 108 | "@types/jsdom": "^16.2.5", 109 | "@types/node": "^10.11.0", 110 | "colors": "^1.3.2", 111 | "commitizen": "^4.2.2", 112 | "coveralls": "^3.0.2", 113 | "cross-env": "^5.2.0", 114 | "cz-conventional-changelog": "^2.1.0", 115 | "husky": "^1.0.1", 116 | "jest": "^26.6.3", 117 | "jest-config": "^26.6.3", 118 | "jest-coverage-badges": "^1.1.2", 119 | "jsdom": "^16.4.0", 120 | "lint-staged": "^8.0.0", 121 | "lodash.camelcase": "^4.3.0", 122 | "madge": "^4.0.1", 123 | "prettier": "^2.2.1", 124 | "prompt": "^1.0.0", 125 | "replace-in-file": "^3.4.2", 126 | "rimraf": "^2.6.2", 127 | "rollup": "^0.67.0", 128 | "rollup-plugin-commonjs": "^9.1.8", 129 | "rollup-plugin-json": "^3.1.0", 130 | "rollup-plugin-node-resolve": "^3.4.0", 131 | "rollup-plugin-sourcemaps": "^0.4.2", 132 | "rollup-plugin-typescript2": "^0.18.0", 133 | "semantic-release": "^17.2.3", 134 | "shelljs": "^0.8.3", 135 | "travis-deploy-once": "^5.0.9", 136 | "ts-jest": "^26.4.4", 137 | "ts-node": "^7.0.1", 138 | "tslint": "^5.11.0", 139 | "tslint-config-prettier": "^1.15.0", 140 | "tslint-config-standard": "^8.0.1", 141 | "typedoc": "^0.19.2", 142 | "typedoc-plugin-external-module-name": "^4.0.5", 143 | "typedoc-plugin-remove-references": "0.0.5", 144 | "typescript": "^4.1.2" 145 | }, 146 | "dependencies": {} 147 | } 148 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Malwoden 2 | 3 | ![alt text](./coverage/badge-lines.svg) 4 | ![alt text](./coverage/badge-statements.svg) 5 | ![alt text](./coverage/badge-functions.svg) 6 | ![alt text](./coverage/badge-branches.svg) 7 | 8 | [Website](https://malwoden.com) | [Docs](https://docs.malwoden.com) 9 | 10 | Malwoden is a roguelike library, meant to perform much of the heavy lifting when creating roguelike games. It takes inspiration from [rot-js](https://ondras.github.io/rot.js/hp), as well as [bracket-lib](https://github.com/thebracket/bracket-lib). ROT still has a number of features we're still building towards, so feel free to take the best parts from each library. 11 | 12 | One of the main goals of this library is to provide a simple, minimalistic `Terminal` package with great support for CP437 tilesets. 13 | This is one area I've found lacking, and hope this library can provide a solid framework for roguelikes and text based games. 14 | The core of the terminal package is based heavily on Bob Nystrom's amazing [malison](https://github.com/munificent/malison) Dart library. 15 | 16 | If you're looking for graphics outside basic ASCII/CP437, [phaser](https://phaser.io/) and [pixi](https://www.pixijs.com/) are both worth checking out. 17 | 18 | --- 19 | 20 | ## Installation 21 | 22 | Malwoden can be downloaded via npm: 23 | 24 | ```sh 25 | # For stable 26 | npm install malwoden 27 | 28 | # For dev builds 29 | npm install malwoden@next 30 | ``` 31 | 32 | If developing malwoden locally, you can use `npm link` to easily use it in another project. 33 | 34 | ```sh 35 | # Inside the malwoden project 36 | npm run start 37 | npm link 38 | 39 | # Inside another project 40 | npm link malwoden 41 | ``` 42 | 43 | --- 44 | 45 | ## Modules 46 | 47 | - FOV - Field of View Algorithms 48 | - Generation - General Map Creation 49 | - GUI - Useful UI Widgets 50 | - Input - Keyboard + Mouse Abstractions 51 | - Calc - Helpful Math Functions, Like Vector Addition 52 | - Pathfinding - Pathfinding Implementations 53 | - Rand - Seedable RNG 54 | - Terminal - Draw Fonts or Tilesets 55 | - Struct - Common Useful Data Structures 56 | 57 | ## Showcase 58 | 59 | - [Mal](https://aedalus.itch.io/malwoden-7drl) - A short 7drl made by the malwoden team showcasing basic features. Source code available at https://github.com/Aedalus/malwoden-7drl 60 | - [Firefighter RL](http://indspenceable.com/7drl-2021/) - A 7drl with fantastic fire mechanics, made by Indspenceable. 61 | 62 | Have a project you'd like added to the list? Feel free to open an issue on the repo with a link! 63 | 64 | ## Resources 65 | 66 | - [Project Setup Tutorial](https://www.youtube.com/watch?v=bN2bI7AlxG0) - A great youtube video made by Rakaneth, showing how to set up a new project from scratch with Malwoden and Webpack. The result can be seen at https://github.com/Rakaneth/malwoden-tut 67 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | import commonjs from "rollup-plugin-commonjs"; 3 | import sourceMaps from "rollup-plugin-sourcemaps"; 4 | import camelCase from "lodash.camelcase"; 5 | import typescript from "rollup-plugin-typescript2"; 6 | import json from "rollup-plugin-json"; 7 | 8 | const pkg = require("./package.json"); 9 | 10 | const libraryName = "malwoden"; 11 | 12 | export default { 13 | input: `src/${libraryName}.ts`, 14 | output: [ 15 | { 16 | file: pkg.main, 17 | name: camelCase(libraryName), 18 | format: "umd", 19 | sourcemap: true, 20 | }, 21 | { file: pkg.module, format: "es", sourcemap: true }, 22 | ], 23 | // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') 24 | external: [], 25 | watch: { 26 | include: "src/**", 27 | }, 28 | plugins: [ 29 | // Allow json resolution 30 | json(), 31 | // Compile TypeScript files 32 | typescript({ declarations: true, useTsconfigDeclarationDir: true }), 33 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) 34 | commonjs(), 35 | // Allow node_modules resolution, so you can use 'external' to control 36 | // which external modules to include in the bundle 37 | // https://github.com/rollup/rollup-plugin-node-resolve#usage 38 | resolve(), 39 | 40 | // Resolve source maps to the original source 41 | sourceMaps(), 42 | ], 43 | }; 44 | -------------------------------------------------------------------------------- /src/calc/index.ts: -------------------------------------------------------------------------------- 1 | export { Vector } from "./vector"; 2 | -------------------------------------------------------------------------------- /src/calc/vector.ts: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "../struct/vector"; 2 | 3 | /** Contains math for common vector operations. */ 4 | export class Vector { 5 | /** 6 | * Returns true if two vectors have the same x and y values. 7 | * @param v1 The first Vector2. 8 | * @param v2 The second Vector2. 9 | */ 10 | static areEqual(v1: Vector2, v2: Vector2): boolean { 11 | return v1.x === v2.x && v1.y === v2.y; 12 | } 13 | 14 | /** 15 | * Returns the distance between two vectors. 16 | * 17 | * If no topology is given, diagonal distance is sqrt(2). 18 | * If topology is four, diagonal distance is 2. 19 | * If topology is eight, diagonal distance is 1. 20 | * 21 | * @param start The starting Vector2. 22 | * @param end The ending Vector2. 23 | * @param topology Can use "four" or "eight" for non-cartesian distances. 24 | */ 25 | static getDistance( 26 | start: Vector2, 27 | end: Vector2, 28 | topology?: "four" | "eight" 29 | ): number { 30 | if (topology === "four") { 31 | return Math.abs(start.x - end.x) + Math.abs(start.y - end.y); 32 | } else if (topology === "eight") { 33 | return Math.max(Math.abs(start.x - end.x), Math.abs(start.y - end.y)); 34 | } else { 35 | const a = start.x - end.x; 36 | const b = start.y - end.y; 37 | return Math.sqrt(a * a + b * b); 38 | } 39 | } 40 | 41 | /** 42 | * Will find the center of an area by averaging all Vectors in the area. 43 | * This point may not be in the area itself, for example in a donut shaped area. 44 | * @param area Vector2 45 | */ 46 | static getCenter(area: Vector2[]): Vector2 { 47 | if (area.length < 1) { 48 | throw new Error("Error: Trying to find center of empty area"); 49 | } 50 | 51 | let sx = 0; 52 | let sy = 0; 53 | 54 | for (let v of area) { 55 | sx += v.x; 56 | sy += v.y; 57 | } 58 | 59 | return { 60 | x: sx / area.length, 61 | y: sy / area.length, 62 | }; 63 | } 64 | 65 | /** 66 | * Will find the position in the area closest to the target 67 | * @param area Vector2[] 68 | * @param target Vector2 69 | * @param topology Either 'four' or 'eight'. Default 'four' 70 | */ 71 | static getClosest( 72 | area: Vector2[], 73 | target: Vector2, 74 | topology: "four" | "eight" = "four" 75 | ): Vector2 { 76 | // Throw an error if area is empty 77 | if (area.length < 1) { 78 | throw new Error( 79 | "Error: Trying to find closest position of an empty area" 80 | ); 81 | } 82 | 83 | // Keep track of the closest we've found. 84 | let minDistance = Infinity; 85 | let closest: Vector2 = area[0]; 86 | 87 | for (let v of area) { 88 | let distance = Vector.getDistance(target, v, topology); 89 | // Found exact one, immediately return 90 | if (distance === 0) { 91 | return { 92 | x: target.x, 93 | y: target.y, 94 | }; 95 | } 96 | 97 | // Closer than currently known 98 | if (distance < minDistance) { 99 | minDistance = distance; 100 | closest = v; 101 | } 102 | } 103 | 104 | return closest; 105 | } 106 | 107 | static add(v1: Vector2, v2: Vector2): Vector2 { 108 | return { 109 | x: v1.x + v2.x, 110 | y: v1.y + v2.y, 111 | }; 112 | } 113 | 114 | static subtract(v1: Vector2, v2: Vector2): Vector2 { 115 | return { 116 | x: v1.x - v2.x, 117 | y: v1.y - v2.y, 118 | }; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/fov/get-ring.spec.ts: -------------------------------------------------------------------------------- 1 | import { getRing4, getRing8 } from "./get-ring"; 2 | 3 | describe("getRing - 4", () => { 4 | it("can compute size 0", () => { 5 | const ring0 = getRing4(0, 0, 0); 6 | expect(ring0).toEqual([{ x: 0, y: 0 }]); 7 | }); 8 | 9 | it("can compute size = 1", () => { 10 | const ring1 = getRing4(0, 0, 1); 11 | expect(ring1).toEqual([ 12 | { x: 1, y: 0 }, 13 | { x: 0, y: -1 }, 14 | { x: -1, y: 0 }, 15 | { x: 0, y: 1 }, 16 | ]); 17 | 18 | const ring1_b = getRing4(1, 1, 1); 19 | expect(ring1_b).toEqual([ 20 | { x: 2, y: 1 }, 21 | { x: 1, y: 0 }, 22 | { x: 0, y: 1 }, 23 | { x: 1, y: 2 }, 24 | ]); 25 | 26 | const ring1_c = getRing4(-1, 1, 1); 27 | expect(ring1_c).toEqual([ 28 | { x: 0, y: 1 }, 29 | { x: -1, y: 0 }, 30 | { x: -2, y: 1 }, 31 | { x: -1, y: 2 }, 32 | ]); 33 | }); 34 | 35 | it("can compute size 2", () => { 36 | const ring2 = getRing4(0, 0, 2); 37 | expect(ring2).toEqual([ 38 | { x: 2, y: 0 }, 39 | { x: 1, y: -1 }, 40 | { x: 0, y: -2 }, 41 | { x: -1, y: -1 }, 42 | { x: -2, y: 0 }, 43 | { x: -1, y: 1 }, 44 | { x: 0, y: 2 }, 45 | { x: 1, y: 1 }, 46 | ]); 47 | 48 | const ring2_b = getRing4(-1, 1, 2); 49 | expect(ring2_b).toEqual([ 50 | { x: 1, y: 1 }, 51 | { x: 0, y: 0 }, 52 | { x: -1, y: -1 }, 53 | { x: -2, y: 0 }, 54 | { x: -3, y: 1 }, 55 | { x: -2, y: 2 }, 56 | { x: -1, y: 3 }, 57 | { x: 0, y: 2 }, 58 | ]); 59 | }); 60 | 61 | it("can compute size 3", () => { 62 | const ring3 = getRing4(0, 0, 3); 63 | expect(ring3).toEqual([ 64 | { x: 3, y: 0 }, 65 | { x: 2, y: -1 }, 66 | { x: 1, y: -2 }, 67 | { x: 0, y: -3 }, 68 | { x: -1, y: -2 }, 69 | { x: -2, y: -1 }, 70 | { x: -3, y: 0 }, 71 | { x: -2, y: 1 }, 72 | { x: -1, y: 2 }, 73 | { x: 0, y: 3 }, 74 | { x: 1, y: 2 }, 75 | { x: 2, y: 1 }, 76 | ]); 77 | 78 | const ring3_b = getRing4(-1, 1, 3); 79 | expect(ring3_b).toEqual([ 80 | { x: 2, y: 1 }, 81 | { x: 1, y: 0 }, 82 | { x: 0, y: -1 }, 83 | { x: -1, y: -2 }, 84 | { x: -2, y: -1 }, 85 | { x: -3, y: 0 }, 86 | { x: -4, y: 1 }, 87 | { x: -3, y: 2 }, 88 | { x: -2, y: 3 }, 89 | { x: -1, y: 4 }, 90 | { x: 0, y: 3 }, 91 | { x: 1, y: 2 }, 92 | ]); 93 | }); 94 | }); 95 | 96 | describe("getRing - 8", () => { 97 | it("can compute size 0", () => { 98 | expect(getRing8(0, 0, 0)).toEqual([{ x: 0, y: 0 }]); 99 | expect(getRing8(1, 2, 0)).toEqual([{ x: 1, y: 2 }]); 100 | }); 101 | it("can compute size 1", () => { 102 | expect(getRing8(0, 0, 1)).toEqual([ 103 | { x: 1, y: 0 }, 104 | { x: 1, y: -1 }, 105 | { x: 0, y: -1 }, 106 | { x: -1, y: -1 }, 107 | { x: -1, y: 0 }, 108 | { x: -1, y: 1 }, 109 | { x: 0, y: 1 }, 110 | { x: 1, y: 1 }, 111 | ]); 112 | expect(getRing8(1, 2, 1)).toEqual([ 113 | { x: 2, y: 2 }, 114 | { x: 2, y: 1 }, 115 | { x: 1, y: 1 }, 116 | { x: 0, y: 1 }, 117 | { x: 0, y: 2 }, 118 | { x: 0, y: 3 }, 119 | { x: 1, y: 3 }, 120 | { x: 2, y: 3 }, 121 | ]); 122 | }); 123 | it("can compute size 2", () => { 124 | expect(getRing8(0, 0, 2)).toEqual([ 125 | { x: 2, y: 0 }, 126 | { x: 2, y: -1 }, 127 | { x: 2, y: -2 }, 128 | { x: 1, y: -2 }, 129 | { x: 0, y: -2 }, 130 | { x: -1, y: -2 }, 131 | { x: -2, y: -2 }, 132 | { x: -2, y: -1 }, 133 | { x: -2, y: 0 }, 134 | { x: -2, y: 1 }, 135 | { x: -2, y: 2 }, 136 | { x: -1, y: 2 }, 137 | { x: 0, y: 2 }, 138 | { x: 1, y: 2 }, 139 | { x: 2, y: 2 }, 140 | { x: 2, y: 1 }, 141 | ]); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /src/fov/get-ring.ts: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "../struct"; 2 | 3 | export function getRing8( 4 | originX: number, 5 | originY: number, 6 | range: number 7 | ): Vector2[] { 8 | if (range === 0) return [{ x: originX, y: originY }]; 9 | 10 | const ring: Vector2[] = []; 11 | const maxX = originX + range; 12 | const minX = originX - range; 13 | const maxY = originY + range; 14 | const minY = originY - range; 15 | 16 | //Start at the 3 o'clock, (0 degrees), rotate all the way around 17 | // Top Right Side, no corner 18 | for (let x = maxX, y = originY; y > minY; y--) { 19 | ring.push({ x, y }); 20 | } 21 | 22 | // Top Side, right to left. Include top corners 23 | for (let x = maxX, y = minY; x >= minX; x--) { 24 | ring.push({ x, y }); 25 | } 26 | 27 | // Left side, no corners 28 | for (let x = minX, y = minY + 1; y < maxY; y++) { 29 | ring.push({ x, y }); 30 | } 31 | 32 | // Bottom side, corners 33 | for (let x = minX, y = maxY; x <= maxX; x++) { 34 | ring.push({ x, y }); 35 | } 36 | 37 | // Right side back to to 0 degrees, no corner 38 | for (let x = maxX, y = maxY - 1; y > originY; y--) { 39 | ring.push({ x, y }); 40 | } 41 | 42 | return ring; 43 | } 44 | 45 | export function getRing4( 46 | originX: number, 47 | originY: number, 48 | range: number 49 | ): Vector2[] { 50 | if (range === 0) { 51 | return [ 52 | { 53 | x: originX, 54 | y: originY, 55 | }, 56 | ]; 57 | } 58 | const ring: Vector2[] = []; 59 | 60 | const maxX = originX + range; 61 | const minX = originX - range; 62 | const maxY = originY + range; 63 | const minY = originY - range; 64 | 65 | // Top right arc 66 | for (let x = maxX, y = originY; x > originX; x--, y--) { 67 | ring.push({ x, y }); 68 | } 69 | 70 | // Top left arc 71 | for (let x = originX, y = minY; x > minX; x--, y++) { 72 | ring.push({ x, y }); 73 | } 74 | 75 | // Bottom left arc 76 | for (let x = minX, y = originY; x < originX; x++, y++) { 77 | ring.push({ x, y }); 78 | } 79 | 80 | // Bottom right arc 81 | for (let x = originX, y = maxY; x < maxX; x++, y--) { 82 | ring.push({ x, y }); 83 | } 84 | 85 | return ring; 86 | } 87 | -------------------------------------------------------------------------------- /src/fov/index.ts: -------------------------------------------------------------------------------- 1 | export { PreciseShadowcasting } from "./precise"; 2 | -------------------------------------------------------------------------------- /src/generation/__snapshots__/cellular-automata-builder.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CellularAutomata Can run a simulation step 1`] = ` 4 | Array [ 5 | 1, 6 | 1, 7 | 1, 8 | 1, 9 | 1, 10 | 1, 11 | 1, 12 | 1, 13 | 1, 14 | 1, 15 | 1, 16 | 0, 17 | 0, 18 | 0, 19 | 1, 20 | 0, 21 | 0, 22 | 0, 23 | 0, 24 | 1, 25 | 1, 26 | 0, 27 | 0, 28 | 1, 29 | 1, 30 | 1, 31 | 1, 32 | 1, 33 | 1, 34 | 1, 35 | 1, 36 | 0, 37 | 0, 38 | 1, 39 | 1, 40 | 1, 41 | 1, 42 | 0, 43 | 1, 44 | 1, 45 | 1, 46 | 0, 47 | 1, 48 | 1, 49 | 0, 50 | 1, 51 | 1, 52 | 0, 53 | 0, 54 | 0, 55 | 1, 56 | 0, 57 | 0, 58 | 0, 59 | 0, 60 | 0, 61 | 0, 62 | 0, 63 | 0, 64 | 1, 65 | 1, 66 | 1, 67 | 1, 68 | 0, 69 | 0, 70 | 0, 71 | 0, 72 | 0, 73 | 0, 74 | 1, 75 | 1, 76 | 0, 77 | 0, 78 | 1, 79 | 0, 80 | 0, 81 | 0, 82 | 0, 83 | 0, 84 | 1, 85 | 1, 86 | 1, 87 | 1, 88 | 1, 89 | 0, 90 | 0, 91 | 0, 92 | 0, 93 | 0, 94 | 1, 95 | 1, 96 | 1, 97 | 1, 98 | 1, 99 | 1, 100 | 1, 101 | 1, 102 | 1, 103 | 0, 104 | 1, 105 | ] 106 | `; 107 | -------------------------------------------------------------------------------- /src/generation/builder.spec.ts: -------------------------------------------------------------------------------- 1 | import { Table } from "../struct"; 2 | import { Builder } from "./builder"; 3 | 4 | describe("builder", () => { 5 | it("can take snapshots", () => { 6 | const b = new Builder({ width: 10, height: 10 }); 7 | const m = b.getMap(); 8 | const v = { x: 1, y: 1 }; 9 | 10 | b.takeSnapshot(); 11 | m.set(v, 10); 12 | b.takeSnapshot(); 13 | m.set(v, 20); 14 | b.takeSnapshot(); 15 | 16 | const s = b.getSnapshots(); 17 | expect(s).toHaveLength(3); 18 | 19 | expect(s[0].get(v)).toEqual(undefined); 20 | expect(s[1].get(v)).toEqual(10); 21 | expect(s[2].get(v)).toEqual(20); 22 | }); 23 | 24 | it("can clear snapshots", () => { 25 | const b = new Builder({ width: 10, height: 10 }); 26 | 27 | expect(b.getSnapshots()).toHaveLength(0); 28 | b.takeSnapshot(); 29 | expect(b.getSnapshots()).toHaveLength(1); 30 | b.clearSnapshots(); 31 | expect(b.getSnapshots()).toHaveLength(0); 32 | }); 33 | 34 | it("can start from an existing map", () => { 35 | const t = new Table(10, 10); 36 | t.fill(1); 37 | 38 | const b = new Builder({ width: 10, height: 10 }); 39 | b.copyFrom(t); 40 | const m = b.getMap(); 41 | 42 | expect(m.get({ x: 1, y: 1 })).toEqual(1); 43 | t.set({ x: 1, y: 1 }, 0); 44 | 45 | expect(m.get({ x: 1, y: 1 })).toEqual(1); 46 | }); 47 | 48 | it("cannot copy between builders of different sizes", () => { 49 | const b = new Builder({ width: 10, height: 10 }); 50 | const b2 = new Builder({ width: 20, height: 20 }); 51 | 52 | expect(() => { 53 | b.copyFrom(b2.getMap()); 54 | }).toThrowError("Cannot copy between builders of different sizes"); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/generation/builder.ts: -------------------------------------------------------------------------------- 1 | import { Table } from "../struct"; 2 | 3 | /** 4 | * Builder represents a base class used to create a new kind of map. 5 | * It is meant to be inherited from, with individual specialized builders 6 | * providing additional information as they generate the map. This basic Builder 7 | * class can still be used for it's snapshot functionality when playing with custom 8 | * map generation. 9 | * 10 | * A builder's type should generally be either a number or numeric enum. 11 | */ 12 | export class Builder { 13 | protected map: Table; 14 | 15 | protected snapshots: Table[] = []; 16 | 17 | /** 18 | * Creates a new Builder 19 | * @param config.width - The map width 20 | * @param config.height - The map height 21 | */ 22 | constructor(config: { width: number; height: number }) { 23 | this.map = new Table(config.width, config.height); 24 | } 25 | 26 | /** 27 | * Returns the internal map. 28 | * @returns - Table 29 | */ 30 | getMap(): Table { 31 | return this.map; 32 | } 33 | 34 | /** 35 | * Takes a snapshot of the current map state. These can 36 | * be retrieved through getSnapshots() 37 | */ 38 | takeSnapshot(): void { 39 | this.snapshots.push(this.map.clone()); 40 | } 41 | 42 | /** 43 | * Returns previously captured snapshots. 44 | * @returns - Table[] 45 | */ 46 | getSnapshots(): Table[] { 47 | return this.snapshots; 48 | } 49 | 50 | /** 51 | * Clear previously captured snapshots. 52 | */ 53 | clearSnapshots(): void { 54 | this.snapshots = []; 55 | } 56 | 57 | /** 58 | * Sets the map values to match a given table. A shallow copy is made 59 | * from the given table. 60 | * @param table 61 | */ 62 | copyFrom(table: Table): void { 63 | if (this.map.isSameSize(table) === false) { 64 | throw new Error("Cannot copy between builders of different sizes"); 65 | } 66 | this.map = table.clone(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/generation/cellular-automata-builder.spec.ts: -------------------------------------------------------------------------------- 1 | import { AleaRNG } from "../rand"; 2 | import { CellularAutomataBuilder } from "./cellular-automata-builder"; 3 | 4 | describe("CellularAutomata", () => { 5 | it("Will create with an empty table", () => { 6 | const width = 10; 7 | const height = 10; 8 | 9 | const a = new CellularAutomataBuilder({ 10 | width, 11 | height, 12 | wallValue: 1, 13 | floorValue: 0, 14 | }); 15 | 16 | for (let x = 0; x < width; x++) { 17 | for (let y = 0; y < height; y++) { 18 | expect(a.getMap().get({ x, y })).toBeUndefined(); 19 | } 20 | } 21 | }); 22 | 23 | it("Will default 1/0 as Alive/Dead values", () => { 24 | const a = new CellularAutomataBuilder({ 25 | width: 10, 26 | height: 10, 27 | wallValue: 1, 28 | floorValue: 0, 29 | }); 30 | expect(a["aliveValue"]).toEqual(1); 31 | expect(a["deadValue"]).toEqual(0); 32 | 33 | const b = new CellularAutomataBuilder({ 34 | width: 10, 35 | height: 10, 36 | wallValue: 5, 37 | floorValue: 4, 38 | }); 39 | expect(b["aliveValue"]).toEqual(5); 40 | expect(b["deadValue"]).toEqual(4); 41 | }); 42 | 43 | it("Can randomize with an initial value", () => { 44 | const width = 10; 45 | const height = 10; 46 | 47 | const a = new CellularAutomataBuilder({ 48 | width: 10, 49 | height: 10, 50 | wallValue: 0, 51 | floorValue: 1, 52 | rng: new AleaRNG("foo"), 53 | }); 54 | 55 | a.randomize(0.5); 56 | 57 | let alive = 0; 58 | let dead = 0; 59 | 60 | const m = a.getMap(); 61 | for (let x = 0; x < width; x++) { 62 | for (let y = 0; y < height; y++) { 63 | const val = m.get({ x, y }); 64 | if (val === 1) alive++; 65 | if (val === 0) dead++; 66 | } 67 | } 68 | 69 | expect(alive + dead).toEqual(width * height); 70 | expect(Math.abs(alive - 50)).toBeLessThanOrEqual(10); 71 | expect(Math.abs(dead - 50)).toBeLessThanOrEqual(10); 72 | }); 73 | 74 | it("Can run a simulation step", () => { 75 | const width = 10; 76 | const height = 10; 77 | const a = new CellularAutomataBuilder({ 78 | width, 79 | height, 80 | wallValue: 1, 81 | floorValue: 0, 82 | rng: new AleaRNG("foo"), 83 | }); 84 | a.randomize(); 85 | 86 | a.doSimulationStep(); 87 | expect(a.getMap().items).toMatchSnapshot(); 88 | }); 89 | 90 | it("Can connect areas", () => { 91 | const a = new CellularAutomataBuilder({ 92 | width: 10, 93 | height: 10, 94 | wallValue: 1, 95 | floorValue: 0, 96 | }); 97 | const m = a.getMap(); 98 | m.fill(1); 99 | m.set({ x: 3, y: 3 }, 0); 100 | m.set({ x: 6, y: 3 }, 0); 101 | 102 | const results = a.connect(); 103 | expect(results).toEqual({ 104 | groups: [[{ x: 3, y: 3 }], [{ x: 6, y: 3 }]], 105 | paths: [ 106 | [ 107 | { x: 6, y: 3, r: 0 }, 108 | { x: 5, y: 3, r: 1 }, 109 | { x: 4, y: 3, r: 2 }, 110 | { x: 3, y: 3, r: 3 }, 111 | ], 112 | ], 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /src/generation/cellular-automata-builder.ts: -------------------------------------------------------------------------------- 1 | import { Table } from "../struct"; 2 | import { IRNG, AleaRNG } from "../rand"; 3 | import { connect, ConnectData } from "./util"; 4 | import { Builder } from "./builder"; 5 | 6 | interface CellularAutomataOptions { 7 | width: number; 8 | height: number; 9 | wallValue: T; 10 | floorValue: T; 11 | rng?: IRNG; 12 | } 13 | 14 | /** Used to create CellularAutomata Maps. */ 15 | export class CellularAutomataBuilder extends Builder { 16 | private aliveValue: T; 17 | private deadValue: T; 18 | 19 | private rng: IRNG; 20 | 21 | /** 22 | * Creates a Cellular Automata Map Generator 23 | * 24 | * @param config.width The width of the map. 25 | * @param config.height The height of the map. 26 | * @param config.aliveValue The value to use for alive tiles 27 | * @param config.deadValue The value to use for dead tiles 28 | */ 29 | constructor(options: CellularAutomataOptions) { 30 | super(options); 31 | 32 | this.aliveValue = options.wallValue; 33 | this.deadValue = options.floorValue; 34 | this.rng = options.rng ?? new AleaRNG(); 35 | } 36 | 37 | /** 38 | * Randomly sets each cell to either alive or dead. 39 | * 40 | * @param isAliveChance The chance for a cell to be set to the 'alive' value. 41 | */ 42 | randomize(isAliveChance = 0.6) { 43 | for (let x = 0; x < this.map.width; x++) { 44 | for (let y = 0; y < this.map.height; y++) { 45 | const isAlive = this.rng.next() > isAliveChance; 46 | if (isAlive) { 47 | this.map.set({ x, y }, this.aliveValue); 48 | } else { 49 | this.map.set({ x, y }, this.deadValue); 50 | } 51 | } 52 | } 53 | } 54 | 55 | private countAliveNeighbors(x: number, y: number): number { 56 | let count = 0; 57 | 58 | for (let i = -1; i < 2; i++) { 59 | for (let j = -1; j < 2; j++) { 60 | let neighbor_x = x + i; 61 | let neighbor_y = y + j; 62 | if (i == 0 && j == 0) { 63 | //this code is supposed to do nothing as it is the focal point we're checking around. 64 | } else if ( 65 | this.map.isInBounds({ x: neighbor_x, y: neighbor_y }) == false 66 | ) { 67 | count++; 68 | } else if ( 69 | this.map.get({ x: neighbor_x, y: neighbor_y }) == this.aliveValue 70 | ) { 71 | count++; 72 | } 73 | } 74 | } 75 | return count; 76 | } 77 | 78 | /** 79 | * Runs a number of simulation steps. 80 | * Each step generally "smooths" the map. 81 | * 82 | * @param stepCount The number of steps to run. 83 | */ 84 | doSimulationStep(stepCount = 1) { 85 | for (let step = 0; step < stepCount; step++) { 86 | const newMap = new Table(this.map.width, this.map.height); 87 | const oldMap = this.map; //this just renames the table to prevent 'this' all over the place. 88 | for (let x = 0; x < oldMap.width; x++) { 89 | for (let y = 0; y < oldMap.height; y++) { 90 | let nbs = this.countAliveNeighbors(x, y); 91 | if (oldMap.get({ x, y }) === this.aliveValue) { 92 | if (nbs < 4) { 93 | newMap.set({ x, y }, this.deadValue); 94 | } else { 95 | newMap.set({ x, y }, this.aliveValue); 96 | } 97 | } else { 98 | if (nbs > 3) { 99 | newMap.set({ x, y }, this.aliveValue); 100 | } else { 101 | newMap.set({ x, y }, this.deadValue); 102 | } 103 | } 104 | } 105 | } 106 | this.map = newMap; 107 | } 108 | } 109 | 110 | /** 111 | * Connects areas of the map to ensure they are all connected. 112 | * 113 | * For instance, if you're using an alive value of 1 for walls, 114 | * then this can connect the dead value of 0 to ensure all 115 | * squares on the map are accessable. 116 | * 117 | * @param value The value to connect (default this.deadValue) 118 | */ 119 | connect(value = this.deadValue): ConnectData { 120 | return connect(this.map, value); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/generation/drunkards-walk-builder.spec.ts: -------------------------------------------------------------------------------- 1 | import { AleaRNG } from "../rand"; 2 | import { DrunkardsWalkBuilder } from "./drunkards-walk-builder"; 3 | 4 | describe("Drunkards Walk", () => { 5 | it("Can create a basic drunkards walk", () => { 6 | const w = new DrunkardsWalkBuilder({ 7 | width: 10, 8 | height: 10, 9 | rng: new AleaRNG("foo"), 10 | topology: "eight", 11 | floorTile: 0, 12 | wallTile: 1, 13 | }); 14 | 15 | expect(w.getPaths()).toEqual([]); 16 | expect(w.getCoverage()).toBe(0); 17 | 18 | const m = w.getMap(); 19 | for (let x = 0; x < 10; x++) { 20 | for (let y = 0; y < 10; y++) { 21 | expect(m.get({ x, y })).toEqual(1); 22 | } 23 | } 24 | }); 25 | 26 | it("Can use topology - 8", () => { 27 | const w = new DrunkardsWalkBuilder({ 28 | width: 10, 29 | height: 10, 30 | rng: new AleaRNG("foo"), 31 | topology: "eight", 32 | wallTile: 0, 33 | floorTile: 1, 34 | }); 35 | 36 | w.walk({ stepsMin: 10, stepsMax: 10, pathCount: 2 }); 37 | 38 | expect(w.getPaths()).toHaveLength(2); 39 | expect(w.getCoverage()).toBeLessThanOrEqual(1); 40 | }); 41 | 42 | it("Can be given an initial point", () => { 43 | const w = new DrunkardsWalkBuilder({ 44 | width: 10, 45 | height: 10, 46 | floorTile: 0, 47 | wallTile: 1, 48 | }); 49 | 50 | w.walk({ stepsMin: 10, stepsMax: 10, start: { x: 1, y: 1 } }); 51 | 52 | expect(w.getPaths()[0][0]).toEqual({ x: 1, y: 1 }); 53 | }); 54 | 55 | it("will only increment covered when it hasn't been there", () => { 56 | const w = new DrunkardsWalkBuilder({ 57 | width: 10, 58 | height: 10, 59 | floorTile: 0, 60 | wallTile: 1, 61 | }); 62 | 63 | w["addPoint"]({ x: 1, y: 1 }); 64 | w["addPoint"]({ x: 1, y: 1 }); 65 | 66 | expect(w.getCoverage()).toBe(1 / 100); 67 | }); 68 | 69 | it("Will stop if coveredCount is reached", () => { 70 | const w = new DrunkardsWalkBuilder({ 71 | width: 10, 72 | height: 10, 73 | floorTile: 0, 74 | wallTile: 1, 75 | }); 76 | 77 | w.walk({ stepsMin: 100, stepsMax: 100, maxCoverage: 5 / 100 }); 78 | 79 | expect(w.getCoverage()).toEqual(5 / 100); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/generation/index.ts: -------------------------------------------------------------------------------- 1 | export { CellularAutomataBuilder } from "./cellular-automata-builder"; 2 | export { DrunkardsWalkBuilder } from "./drunkards-walk-builder"; 3 | export { BSPDungeonBuilder, BSPDungeonNode } from "./bsp-dungeon-builder"; 4 | -------------------------------------------------------------------------------- /src/generation/util.spec.ts: -------------------------------------------------------------------------------- 1 | import { Rect } from "../struct/rect"; 2 | import { getSimpleHallwayFromRooms } from "./util"; 3 | 4 | describe("getSimpleHallwayFromRooms", () => { 5 | // A B 6 | // C D 7 | const roomA = new Rect({ x: 1, y: 1 }, { x: 1, y: 1 }); 8 | const roomB = new Rect({ x: 3, y: 1 }, { x: 3, y: 1 }); 9 | const roomC = new Rect({ x: 1, y: 3 }, { x: 1, y: 3 }); 10 | const roomD = new Rect({ x: 3, y: 3 }, { x: 3, y: 3 }); 11 | 12 | it("can create a hallway from basic rooms - horizontal", () => { 13 | expect(getSimpleHallwayFromRooms(roomA, roomB)).toEqual([ 14 | { x: 1, y: 1 }, 15 | { x: 2, y: 1 }, 16 | { x: 3, y: 1 }, 17 | ]); 18 | }); 19 | 20 | it("can create a hallway from rooms - diagonal", () => { 21 | expect(getSimpleHallwayFromRooms(roomA, roomD)).toEqual([ 22 | { x: 1, y: 1 }, 23 | { x: 1, y: 2 }, 24 | { x: 1, y: 3 }, 25 | { x: 2, y: 3 }, 26 | { x: 3, y: 3 }, 27 | ]); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/generation/util.ts: -------------------------------------------------------------------------------- 1 | import * as Calc from "../calc"; 2 | import { AStar } from "../pathfinding/astar"; 3 | import { IRNG } from "../rand"; 4 | import { Table, Vector2 } from "../struct"; 5 | import { Rect } from "../struct/rect"; 6 | 7 | export interface ConnectData { 8 | groups: Vector2[][]; 9 | paths: Vector2[][]; 10 | } 11 | 12 | /** 13 | * Connects areas of the map to ensure they are all connected. 14 | * 15 | * For instance, if you're using an alive value of 1 for walls, 16 | * then this can connect the dead value of 0 to ensure all 17 | * squares on the map are accessable. 18 | * 19 | * @param value The value to connect (default this.deadValue) 20 | */ 21 | export function connect(table: Table, value: T): ConnectData { 22 | const spacesToConnect = new Set(); 23 | const groups: Vector2[][] = []; 24 | const paths: Vector2[][] = []; 25 | 26 | // Get all spaces with the value 27 | for (let x = 0; x < table.width; x++) { 28 | for (let y = 0; y < table.height; y++) { 29 | if (table.get({ x, y }) === value) { 30 | spacesToConnect.add(`${x}:${y}`); 31 | } 32 | } 33 | } 34 | 35 | // Figure out which groups there are 36 | while (spacesToConnect.size > 0) { 37 | const [v] = Array.from(spacesToConnect.entries())[0]; 38 | const [x, y] = v.split(":"); 39 | const position = { 40 | x: Number.parseInt(x), 41 | y: Number.parseInt(y), 42 | }; 43 | 44 | // Grab an area, then remove those tiles from the spacesToConnect 45 | const selection = table.floodFillSelect(position); 46 | groups.push(selection); 47 | for (let s of selection) { 48 | spacesToConnect.delete(`${s.x}:${s.y}`); 49 | } 50 | } 51 | 52 | // spacesToConnect is now empty. 53 | // Each group in groups is an isolated set of tiles 54 | 55 | for (let i = 0; i < groups.length; i++) { 56 | // Ignore the last group 57 | if (i === groups.length - 1) break; 58 | const current = groups[i]; 59 | const next = groups[i + 1]; 60 | 61 | // Get the center point from each area 62 | const currentCenter = Calc.Vector.getCenter(current); 63 | const currentPoint = Calc.Vector.getClosest(current, currentCenter); 64 | const nextCenter = Calc.Vector.getCenter(next); 65 | const nextPoint = Calc.Vector.getClosest(next, nextCenter); 66 | 67 | // Get two points that are close to the edge for each section 68 | const closestCurrent = Calc.Vector.getClosest(next, currentPoint); 69 | const closestNext = Calc.Vector.getClosest(current, nextPoint); 70 | 71 | const a = new AStar({ topology: "four" }); 72 | const connection = a.compute(closestCurrent, closestNext); 73 | 74 | // Connect the paths 75 | if (!connection) throw new Error("Error: Could not connect cell areas"); 76 | for (let v of connection) { 77 | table.set(v, value); 78 | } 79 | 80 | paths.push(connection); 81 | } 82 | 83 | return { 84 | groups, 85 | paths, 86 | }; 87 | } 88 | 89 | export function getSimpleHallwayFromRooms(a: Rect, b: Rect): Vector2[] { 90 | const hallway: Vector2[] = []; 91 | 92 | const start = a.center(); 93 | const end = b.center(); 94 | 95 | const minX = Math.min(start.x, end.x); 96 | const maxX = Math.max(start.x, end.x); 97 | 98 | const minY = Math.min(start.y, end.y); 99 | const maxY = Math.max(start.y, end.y); 100 | 101 | // Connect the vertical 102 | // Connect the horizontal 103 | for (let y = minY; y <= maxY; y++) { 104 | hallway.push({ x: start.x, y }); 105 | } 106 | 107 | for (let x = minX + 1; x <= maxX; x++) { 108 | hallway.push({ x: x, y: end.y }); 109 | } 110 | 111 | return hallway; 112 | } 113 | -------------------------------------------------------------------------------- /src/gui/bar-widget.spec.ts: -------------------------------------------------------------------------------- 1 | import { MemoryTerminal } from "../terminal/memory-terminal"; 2 | import { BarWidget, getRoundedPercent } from "./bar-widget"; 3 | 4 | describe("getRoundedPercent", () => { 5 | it("Can get the floor", () => { 6 | expect(getRoundedPercent(0.12, 10, "down")).toEqual(0.1); 7 | expect(getRoundedPercent(0.12, 10, "up")).toEqual(0.2); 8 | expect(getRoundedPercent(0.12, 10, "default")).toEqual(0.1); 9 | }); 10 | }); 11 | 12 | describe("BarWidget", () => { 13 | it("Can draw a bar", () => { 14 | const terminal = new MemoryTerminal({ width: 10, height: 10 }); 15 | 16 | const w = new BarWidget({ 17 | initialState: { 18 | maxValue: 10, 19 | width: 10, 20 | }, 21 | }); 22 | 23 | w.onDraw(); 24 | w.setTerminal(terminal); 25 | w.onDraw(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/gui/bar-widget.ts: -------------------------------------------------------------------------------- 1 | import { CharCode, Color, Glyph } from "../terminal"; 2 | import { Widget, WidgetConfig } from "./widget"; 3 | 4 | export type RoundMode = "up" | "down" | "default"; 5 | 6 | export interface BarWidgetState { 7 | // Bounds 8 | /** The width of the bar */ 9 | width: number; 10 | 11 | // values 12 | /** The minimum value allowed. Default 0. */ 13 | minValue?: number; 14 | /** The maximum value allowed. */ 15 | maxValue: number; 16 | /** The current value. Default 0. */ 17 | currentValue?: number; 18 | /** The mode to use for rounding. up | down | default */ 19 | roundMode?: RoundMode; 20 | 21 | // Glyphs 22 | /** An optional glyph to use for each filled square. Default white square. */ 23 | foreGlyph?: Glyph; 24 | /** An optional glyph to use for each empty square. Default gray. */ 25 | backGlyph?: Glyph; 26 | } 27 | 28 | export function getRoundedPercent( 29 | percent: number, 30 | divisor: number, 31 | round: RoundMode 32 | ) { 33 | let roundFunc = Math.round; 34 | if (round === "up") { 35 | roundFunc = Math.ceil; 36 | } else if (round === "down") { 37 | roundFunc = Math.floor; 38 | } 39 | 40 | return roundFunc(percent * divisor) / divisor; 41 | } 42 | 43 | /** 44 | * Represents a bar, like in a loading/progress indicator 45 | * or hp bar. 46 | */ 47 | export class BarWidget extends Widget { 48 | state: Required; 49 | 50 | constructor(config: WidgetConfig) { 51 | super(config); 52 | 53 | this.state = { 54 | roundMode: "default", 55 | minValue: 0, 56 | currentValue: 0, 57 | foreGlyph: Glyph.fromCharCode(CharCode.fullBlock, Color.White), 58 | backGlyph: Glyph.fromCharCode(CharCode.fullBlock, Color.Gray), 59 | ...config.initialState, 60 | }; 61 | } 62 | 63 | onDraw(): void { 64 | if (!this.terminal) return; 65 | 66 | const origin = this.getAbsoluteOrigin(); 67 | const percent = 68 | (this.state.currentValue - this.state.minValue!) / 69 | (this.state.maxValue - this.state.minValue!); 70 | const roundedPercent = getRoundedPercent( 71 | percent, 72 | this.state.width + 1, 73 | this.state.roundMode! 74 | ); 75 | 76 | for (let x = origin.x; x <= origin.x + this.state.width; x++) { 77 | const p = (x - origin.x) / this.state.width; 78 | if (p <= roundedPercent) { 79 | this.terminal.drawGlyph({ x, y: origin.y }, this.state.foreGlyph); 80 | } else { 81 | this.terminal.drawGlyph({ x, y: origin.y }, this.state.backGlyph); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/gui/button-widget.spec.ts: -------------------------------------------------------------------------------- 1 | import { JSDOM } from "jsdom"; 2 | import { MouseHandler, MouseHandlerEvent } from "../input"; 3 | import { MemoryTerminal } from "../terminal/memory-terminal"; 4 | import { ButtonWidget, HoverState } from "./button-widget"; 5 | import { Color } from "../terminal"; 6 | import { setupTestDom } from "../input/test-utils.spec"; 7 | 8 | describe("ButtonWidget", () => { 9 | beforeEach(setupTestDom); 10 | 11 | it("can draw a basic button", () => { 12 | const t = new MemoryTerminal({ width: 10, height: 10 }); 13 | const b = new ButtonWidget({ initialState: { text: "button" } }); 14 | 15 | b.onDraw(); 16 | b.setTerminal(t); 17 | 18 | b.onDraw(); 19 | 20 | b.setState({ padding: 1, borderStyle: "single-bar" }); 21 | b.onDraw(); 22 | b.setState({ borderStyle: "double-bar" }); 23 | b.onDraw(); 24 | }); 25 | 26 | it("can get padding", () => { 27 | const b = new ButtonWidget({ initialState: { text: "button" } }).setState({ 28 | padding: 1, 29 | }); 30 | 31 | expect(b["getPadding"]()).toEqual(1); 32 | b.setState({ padding: undefined }); 33 | expect(b["getPadding"]()).toEqual(0); 34 | }); 35 | 36 | it("can get bounds", () => { 37 | const b = new ButtonWidget({ initialState: { text: "Hello" } }); 38 | 39 | const bounds = b["getBounds"](); 40 | expect(bounds.v1).toEqual({ x: 0, y: 0 }); 41 | expect(bounds.v2).toEqual({ x: 4, y: 0 }); 42 | }); 43 | 44 | it("can get the proper mouse handler from mouse state", () => { 45 | const h = new MouseHandler(); 46 | const t = new MemoryTerminal({ width: 10, height: 10 }); 47 | const b = new ButtonWidget({ initialState: { text: "Hello" } }) 48 | .setTerminal(t) 49 | .setMouseHandler(h); 50 | 51 | const bounds = b["getBounds"](); 52 | expect(bounds.v1).toEqual({ x: 0, y: 0 }); 53 | expect(bounds.v2).toEqual({ x: 4, y: 0 }); 54 | 55 | const mouseState = b["getMouseStateFromMouseHandler"](h, t); 56 | expect(mouseState).toEqual(HoverState.Hover); 57 | 58 | h["_isDown"].add(0); 59 | expect(bounds.contains(h.getPos())); 60 | debugger; 61 | expect(b["getMouseStateFromMouseHandler"](h, t)).toEqual(HoverState.Down); 62 | 63 | h["x"] = -1; 64 | h["y"] = -1; 65 | expect(b["getMouseStateFromMouseHandler"](h, t)).toEqual(HoverState.None); 66 | }); 67 | 68 | it("Will get the proper color depending on HoverState", () => { 69 | const foreColor = Color.Red; 70 | const backColor = Color.Blue; 71 | const downColor = Color.Green; 72 | const hoverColor = Color.Yellow; 73 | const b = new ButtonWidget({ 74 | initialState: { 75 | text: "Hello", 76 | foreColor, 77 | backColor, 78 | downColor, 79 | hoverColor, 80 | }, 81 | }); 82 | 83 | expect(b["getBackColor"](HoverState.None)).toEqual(backColor); 84 | expect(b["getBackColor"](HoverState.Hover)).toEqual(hoverColor); 85 | expect(b["getBackColor"](HoverState.Down)).toEqual(downColor); 86 | expect(b["getBackColor"](-1 as any)).toEqual(undefined); 87 | }); 88 | 89 | it("can get a mouse click", () => { 90 | const h = new MouseHandler(); 91 | const t = new MemoryTerminal({ width: 10, height: 10 }); 92 | const b = new ButtonWidget({ initialState: { text: "Hello" } }); 93 | 94 | const event: MouseHandlerEvent = { 95 | x: 0, 96 | y: 0, 97 | type: "mousedown", 98 | button: 0, 99 | }; 100 | 101 | b.onMouseClick(event); 102 | b.setTerminal(t); 103 | b.onMouseClick(event); 104 | b.setState({ onClick: () => {} }); 105 | 106 | expect(b.onMouseClick(event)).toEqual(true); 107 | event.x = -1; 108 | expect(b.onMouseClick(event)).toEqual(false); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/gui/button-widget.ts: -------------------------------------------------------------------------------- 1 | import { MouseHandler, MouseHandlerEvent } from "../input"; 2 | import { Rect } from "../struct"; 3 | import { BaseTerminal, CharCode, Color, Glyph } from "../terminal"; 4 | import { Widget, WidgetConfig } from "./widget"; 5 | import * as Calc from "../calc"; 6 | import { BorderStyles, drawBorder } from "./util/draw-borders"; 7 | 8 | export interface ButtonWidgetState { 9 | text: string; 10 | backColor?: Color; 11 | foreColor?: Color; 12 | hoverColor?: Color; 13 | downColor?: Color; 14 | padding?: number; 15 | 16 | onClick?: () => void; 17 | mouseButton?: number; 18 | 19 | borderStyle?: BorderStyles; 20 | } 21 | 22 | export enum HoverState { 23 | None = 0, 24 | Hover = 1, 25 | Down = 2, 26 | } 27 | 28 | export class ButtonWidget extends Widget { 29 | constructor(config: WidgetConfig) { 30 | super(config); 31 | this.state = { 32 | foreColor: Color.White, 33 | hoverColor: Color.DarkGray, 34 | downColor: Color.Black, 35 | backColor: Color.Black, 36 | padding: 0, 37 | ...config.initialState, 38 | }; 39 | } 40 | 41 | private getPadding(): number { 42 | return this.state.padding ?? 0; 43 | } 44 | 45 | private getBounds(): Rect { 46 | return Rect.FromWidthHeight( 47 | this.absoluteOrigin, 48 | this.getPadding() * 2 + this.state.text.length, 49 | this.getPadding() * 2 + 1 50 | ); 51 | } 52 | 53 | private getMouseStateFromMouseHandler( 54 | mouseHandler?: MouseHandler, 55 | terminal?: BaseTerminal 56 | ): HoverState { 57 | if (!mouseHandler || !terminal) return HoverState.None; 58 | 59 | const mousePos = mouseHandler.getPos(); 60 | const terminalPos = terminal.windowToTilePoint(mousePos); 61 | const mouseDown = mouseHandler.isMouseDown(); 62 | 63 | if (this.getBounds().contains(terminalPos)) { 64 | if (mouseDown) { 65 | return HoverState.Down; 66 | } else { 67 | return HoverState.Hover; 68 | } 69 | } else { 70 | return HoverState.None; 71 | } 72 | } 73 | 74 | private getBackColor(mouseState: HoverState): Color | undefined { 75 | if (mouseState === HoverState.None) { 76 | return this.state.backColor; 77 | } 78 | 79 | if (mouseState === HoverState.Hover) { 80 | return this.state.hoverColor; 81 | } 82 | 83 | if (mouseState === HoverState.Down) { 84 | return this.state.downColor; 85 | } 86 | } 87 | 88 | onMouseClick(event: MouseHandlerEvent): boolean { 89 | if (!this.terminal || !this.state.onClick) return false; 90 | const tilePos = this.terminal.windowToTilePoint(event); 91 | if (this.getBounds().contains(tilePos)) { 92 | this.state.onClick(); 93 | return true; 94 | } 95 | return false; 96 | } 97 | 98 | onDraw(): void { 99 | if (!this.terminal) return; 100 | 101 | const bounds = this.getBounds(); 102 | 103 | const hoverState = this.getMouseStateFromMouseHandler( 104 | this.mouseHandler, 105 | this.terminal 106 | ); 107 | const backColor = this.getBackColor(hoverState); 108 | 109 | const g = Glyph.fromCharCode( 110 | CharCode.space, 111 | this.state.foreColor, 112 | backColor 113 | ); 114 | 115 | for (let y = bounds.v1.y; y <= bounds.v2.y; y++) { 116 | for (let x = bounds.v1.x; x <= bounds.v2.x; x++) { 117 | this.terminal.drawGlyph({ x, y }, g); 118 | } 119 | } 120 | 121 | this.terminal.writeAt( 122 | Calc.Vector.add(bounds.v1, { 123 | x: this.getPadding(), 124 | y: this.getPadding(), 125 | }), 126 | this.state.text, 127 | this.state.foreColor, 128 | backColor 129 | ); 130 | 131 | // Draw borders 132 | if (this.getPadding() > 0 && this.state.borderStyle) { 133 | drawBorder({ 134 | terminal: this.terminal, 135 | style: this.state.borderStyle, 136 | foreColor: this.state.foreColor, 137 | backColor, 138 | bounds: this.getBounds(), 139 | }); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/gui/container-widget.spec.ts: -------------------------------------------------------------------------------- 1 | import { MemoryTerminal } from "../terminal/memory-terminal"; 2 | import { ContainerWidget } from "./container-widget"; 3 | 4 | describe("ContainerWidget", () => { 5 | it("Can create a new container", () => { 6 | const w = new ContainerWidget(); 7 | }); 8 | 9 | it("will render nothing", () => { 10 | const terminal = new MemoryTerminal({ width: 10, height: 10 }); 11 | const w = new ContainerWidget(); 12 | 13 | w.onDraw(); 14 | w.setTerminal(terminal); 15 | w.onDraw(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/gui/container-widget.ts: -------------------------------------------------------------------------------- 1 | import { WidgetConfig } from "."; 2 | import { Widget } from "./widget"; 3 | 4 | export interface ContainerWidgetState {} 5 | 6 | /** 7 | * An empty widget, used to hold other widgets. 8 | */ 9 | export class ContainerWidget extends Widget { 10 | constructor(config?: Partial>) { 11 | super({ 12 | initialState: {}, 13 | ...config, 14 | }); 15 | } 16 | 17 | onDraw(): void { 18 | return; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/gui/index.ts: -------------------------------------------------------------------------------- 1 | export { BarWidget, BarWidgetState } from "./bar-widget"; 2 | export { ContainerWidget, ContainerWidgetState } from "./container-widget"; 3 | export { LabelWidget, LabelWidgetState } from "./label-widget"; 4 | export { PanelWidget, PanelWidgetState } from "./panel-widget"; 5 | export { TextWidget, TextWidgetState } from "./text-widget"; 6 | export { Widget, WidgetConfig } from "./widget"; 7 | export { ButtonWidget, ButtonWidgetState } from "./button-widget"; 8 | -------------------------------------------------------------------------------- /src/gui/label-widget.spec.ts: -------------------------------------------------------------------------------- 1 | import { MemoryTerminal } from "../terminal/memory-terminal"; 2 | import { LabelWidget } from "./label-widget"; 3 | 4 | describe("LabelWidget", () => { 5 | it("Can render a basic label", () => { 6 | const terminal = new MemoryTerminal({ width: 20, height: 20 }); 7 | const w = new LabelWidget({ 8 | initialState: { text: "Hello!", direction: "left" }, 9 | }); 10 | 11 | w.onDraw(); 12 | 13 | w.setTerminal(terminal); 14 | w.onDraw(); 15 | 16 | w.setState({ direction: "right" }); 17 | 18 | w.onDraw(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/gui/label-widget.ts: -------------------------------------------------------------------------------- 1 | import { WidgetConfig } from "."; 2 | import { BaseTerminal, CharCode } from "../terminal"; 3 | import { Color } from "../terminal/color"; 4 | import { Widget } from "./widget"; 5 | 6 | export interface LabelWidgetState { 7 | /** The text to display, should be short. */ 8 | text: string; 9 | 10 | /** The direction the text should be, relative to the origin. */ 11 | direction: "left" | "right"; 12 | 13 | // Colors 14 | /** The primary color of the label. Default white. */ 15 | foreColor?: Color; 16 | /** The secondary color of the label. Default black. */ 17 | backColor?: Color; 18 | } 19 | 20 | /** 21 | * Represents a label drawn on the screen, often for help text. 22 | * The direction can be controlled to make sure the label doesn't 23 | * get cut off on the edge of the screen. 24 | */ 25 | export class LabelWidget extends Widget { 26 | constructor(config: WidgetConfig) { 27 | super(config); 28 | this.state = { 29 | foreColor: Color.White, 30 | backColor: Color.Black, 31 | ...config.initialState, 32 | }; 33 | } 34 | private renderLeftLabel(terminal: BaseTerminal): void { 35 | const origin = this.getAbsoluteOrigin(); 36 | const { text, foreColor, backColor } = this.state; 37 | const start = { x: origin.x - text.length - 2, y: origin.y }; 38 | 39 | terminal.writeAt(start, text, foreColor, backColor); 40 | terminal.drawCharCode( 41 | { x: start.x + text.length, y: origin.y }, 42 | CharCode.fullBlock, 43 | backColor 44 | ); 45 | terminal.drawCharCode( 46 | { x: start.x + text.length + 1, y: origin.y }, 47 | CharCode.rightwardsArrow, 48 | backColor, 49 | foreColor 50 | ); 51 | } 52 | 53 | private renderRightLabel(terminal: BaseTerminal): void { 54 | const origin = this.getAbsoluteOrigin(); 55 | const { text, foreColor, backColor } = this.state; 56 | const start = { x: origin.x + 1, y: origin.y }; 57 | 58 | terminal.drawCharCode(start, CharCode.leftwardsArrow, backColor, foreColor); 59 | terminal.drawCharCode( 60 | { x: start.x + 1, y: start.y }, 61 | CharCode.fullBlock, 62 | backColor, 63 | foreColor 64 | ); 65 | terminal.writeAt( 66 | { x: start.x + 2, y: start.y }, 67 | text, 68 | foreColor, 69 | backColor 70 | ); 71 | } 72 | 73 | onDraw(): void { 74 | if (!this.terminal) return; 75 | if (this.state.direction === "left") { 76 | this.renderLeftLabel(this.terminal); 77 | } else { 78 | this.renderRightLabel(this.terminal); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/gui/panel-widget.spec.ts: -------------------------------------------------------------------------------- 1 | import { MemoryTerminal } from "../terminal/memory-terminal"; 2 | import { PanelWidget } from "./panel-widget"; 3 | 4 | describe("PanelWidget", () => { 5 | it("Can render a basic panel", () => { 6 | const terminal = new MemoryTerminal({ width: 20, height: 20 }); 7 | const p = new PanelWidget({ initialState: { width: 10, height: 10 } }); 8 | 9 | p.onDraw(); 10 | p.setTerminal(terminal); 11 | p.onDraw(); 12 | }); 13 | 14 | it("Can render a border panel", () => { 15 | const terminal = new MemoryTerminal({ width: 20, height: 20 }); 16 | const p = new PanelWidget({ 17 | initialState: { width: 10, height: 10, borderStyle: "double-bar" }, 18 | }); 19 | p.setTerminal(terminal); 20 | p.onDraw(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/gui/panel-widget.ts: -------------------------------------------------------------------------------- 1 | import { Rect, Vector2 } from "../struct"; 2 | import { BaseTerminal, CharCode, Color, Glyph } from "../terminal"; 3 | import { BorderStyles, drawBorder } from "./util/draw-borders"; 4 | import { Widget, WidgetConfig } from "./widget"; 5 | 6 | export interface PanelWidgetState { 7 | // Bounds 8 | /** The width of the panel */ 9 | width: number; 10 | /** The height of the panel */ 11 | height: number; 12 | 13 | // Colors 14 | /** The color of any border. Default White. */ 15 | foreColor?: Color; 16 | /** The color of the panel. Default Black */ 17 | backColor?: Color; 18 | 19 | // Borders 20 | /** An optional border style. Default undefined for none. */ 21 | borderStyle?: BorderStyles; 22 | } 23 | 24 | /** 25 | * Represents a rectangle drawn on the screen, often with a border. 26 | */ 27 | export class PanelWidget extends Widget { 28 | constructor(config: WidgetConfig) { 29 | super(config); 30 | 31 | this.state = { 32 | foreColor: Color.White, 33 | backColor: Color.Black, 34 | ...config.initialState, 35 | }; 36 | } 37 | 38 | private getAbsTopLeft(): Vector2 { 39 | return this.getAbsoluteOrigin(); 40 | } 41 | 42 | private getAbsBottomRight(): Vector2 { 43 | const o = this.getAbsoluteOrigin(); 44 | return { 45 | x: o.x + this.state.width - 1, 46 | y: o.y + this.state.height - 1, 47 | }; 48 | } 49 | 50 | onDraw(): void { 51 | if (!this.terminal) return; 52 | 53 | const { backColor, foreColor } = this.state; 54 | 55 | const bgGlyph = new Glyph(" ", foreColor, backColor); 56 | const tl = this.getAbsTopLeft(); 57 | const br = this.getAbsBottomRight(); 58 | this.terminal.fill(tl, br, bgGlyph); 59 | 60 | if (this.state.borderStyle) { 61 | drawBorder({ 62 | terminal: this.terminal, 63 | foreColor, 64 | backColor, 65 | style: this.state.borderStyle, 66 | bounds: new Rect(this.getAbsTopLeft(), this.getAbsBottomRight()), 67 | }); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/gui/text-widget.spec.ts: -------------------------------------------------------------------------------- 1 | import { MemoryTerminal } from "../terminal/memory-terminal"; 2 | import { wrapText, truncateText, TextWidget } from "./text-widget"; 3 | 4 | describe("truncateText", () => { 5 | it("Can truncate text without ellipsis", () => { 6 | expect( 7 | truncateText({ 8 | text: "Hello World", 9 | truncateAfter: 8, 10 | addEllipsis: false, 11 | }) 12 | ).toEqual("Hello Wo"); 13 | }); 14 | it("Can truncate text with ellipsis", () => { 15 | expect( 16 | truncateText({ 17 | text: "Hello World", 18 | truncateAfter: 8, 19 | addEllipsis: true, 20 | }) 21 | ).toEqual("Hello..."); 22 | }); 23 | }); 24 | 25 | describe("splitAtWrap", () => { 26 | it("Can split a larger body of text", () => { 27 | const text = "Hello world how are you doing today?"; 28 | expect(wrapText({ text, wrapAt: 10 })).toEqual([ 29 | "Hello", 30 | "world how", 31 | "are you", 32 | "doing", 33 | "today?", 34 | ]); 35 | }); 36 | it("will pass back an empty array for no text", () => { 37 | expect( 38 | wrapText({ 39 | text: "", 40 | wrapAt: 4, 41 | }) 42 | ).toEqual([]); 43 | }); 44 | }); 45 | 46 | describe("TextWidget", () => { 47 | it("Can render basic text", () => { 48 | const terminal = new MemoryTerminal({ width: 10, height: 10 }); 49 | const w = new TextWidget({ 50 | initialState: { text: "Hello World" }, 51 | }); 52 | 53 | w.onDraw(); 54 | w.setTerminal(terminal); 55 | w.onDraw(); 56 | }); 57 | 58 | it("Can truncate text", () => { 59 | const terminal = new MemoryTerminal({ width: 10, height: 10 }); 60 | const w = new TextWidget({ 61 | initialState: { text: "Hello World", truncateAfter: 8 }, 62 | }); 63 | 64 | w.onDraw(); 65 | w.setTerminal(terminal); 66 | w.onDraw(); 67 | }); 68 | 69 | it("Can wrap text", () => { 70 | const terminal = new MemoryTerminal({ width: 10, height: 10 }); 71 | const w = new TextWidget({ 72 | initialState: { text: "Hello World", wrapAt: 8 }, 73 | }); 74 | 75 | w.onDraw(); 76 | w.setTerminal(); 77 | w.onDraw(); 78 | }); 79 | 80 | it("Can get lines", () => { 81 | const w = new TextWidget({ initialState: { text: "Hello World" } }); 82 | 83 | expect(w["getLines"]("Hello World")).toEqual(["Hello World"]); 84 | 85 | w.setState({ wrapAt: 3 }); 86 | expect(w["getLines"]("Hello World")).toEqual(["Hello", "World"]); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/gui/text-widget.ts: -------------------------------------------------------------------------------- 1 | import { WidgetConfig } from "."; 2 | import { Color } from "../terminal"; 3 | import { Widget } from "./widget"; 4 | 5 | export function truncateText(config: { 6 | text: string; 7 | truncateAfter: number; 8 | addEllipsis: boolean; 9 | }): string { 10 | if (config.addEllipsis) { 11 | return `${config.text.substring(0, config.truncateAfter - 3)}...`; 12 | } else { 13 | return config.text.substring(0, config.truncateAfter); 14 | } 15 | } 16 | 17 | export function wrapText(config: { text: string; wrapAt: number }): string[] { 18 | const lines: string[] = []; 19 | const words = config.text.split(" "); 20 | 21 | let current = ""; 22 | while (words.length) { 23 | const next = words.shift()!; 24 | const nextWithSpace = ` ${next}`; 25 | 26 | // ensure we always get at least one word 27 | if (current === "") { 28 | current += next; 29 | continue; 30 | } else if (current.length + nextWithSpace.length > config.wrapAt) { 31 | // if we need to line break 32 | lines.push(current); 33 | current = next; 34 | } else { 35 | // otherwise add it on 36 | current += nextWithSpace; 37 | } 38 | } 39 | 40 | if (current.length) { 41 | lines.push(current); 42 | } 43 | return lines; 44 | } 45 | 46 | /** 47 | * The State of a TextWidget 48 | */ 49 | export interface TextWidgetState { 50 | /** The text to display */ 51 | text: string; 52 | 53 | // Colors 54 | /** The color to use for the text. Default White. */ 55 | foreColor?: Color; 56 | /** The color to use for the background. Default Black*/ 57 | backColor?: Color; 58 | 59 | // Truncate 60 | /** Truncate the text after this amount */ 61 | truncateAfter?: number; 62 | /** Change the last three chars of a truncated text to ... */ 63 | truncateAddEllipsis?: boolean; 64 | /** Wrap the text after this number */ 65 | wrapAt?: number; 66 | } 67 | 68 | /** 69 | * Represents text to draw to the screen, potentially truncated or wrapped. 70 | */ 71 | export class TextWidget extends Widget { 72 | constructor(config: WidgetConfig) { 73 | super(config); 74 | this.state = { 75 | foreColor: Color.White, 76 | backColor: Color.Black, 77 | ...config.initialState, 78 | }; 79 | } 80 | private getLines(text: string): string[] { 81 | if (this.state.wrapAt) return wrapText({ text, wrapAt: this.state.wrapAt }); 82 | else return [text]; 83 | } 84 | 85 | private getText(): string { 86 | if (this.state.truncateAfter === undefined) return this.state.text; 87 | 88 | return truncateText({ 89 | text: this.state.text, 90 | truncateAfter: this.state.truncateAfter, 91 | addEllipsis: !!this.state.truncateAddEllipsis, 92 | }); 93 | } 94 | 95 | onDraw(): void { 96 | if (!this.terminal) return; 97 | 98 | const origin = this.getAbsoluteOrigin(); 99 | 100 | const text = this.getText(); 101 | const lines = this.getLines(text); 102 | 103 | for (let y = 0; y < lines.length; y++) { 104 | const line = lines[y]; 105 | this.terminal.writeAt( 106 | { x: origin.x, y: origin.y + y }, 107 | line, 108 | this.state.foreColor, 109 | this.state.backColor 110 | ); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/gui/util/draw-borders.ts: -------------------------------------------------------------------------------- 1 | import { Rect } from "../../struct"; 2 | import { BaseTerminal, CharCode, Color, Glyph } from "../../terminal"; 3 | 4 | export type BorderStyles = "double-bar" | "single-bar"; 5 | 6 | interface BorderStyleTiles { 7 | topLeftCorner: CharCode; 8 | topRightCorner: CharCode; 9 | bottomLeftCorner: CharCode; 10 | bottomRightCorner: CharCode; 11 | verticalBarsLeft: CharCode; 12 | verticalBarsRight: CharCode; 13 | horizontalBarsTop: CharCode; 14 | horizontalBarsBottom: CharCode; 15 | } 16 | 17 | const tileMap: { [style in BorderStyles]: BorderStyleTiles } = { 18 | "double-bar": { 19 | topLeftCorner: CharCode.boxDrawingsDoubleDownAndRight, 20 | topRightCorner: CharCode.boxDrawingsDoubleDownAndLeft, 21 | bottomLeftCorner: CharCode.boxDrawingsDoubleUpAndRight, 22 | bottomRightCorner: CharCode.boxDrawingsDoubleUpAndLeft, 23 | verticalBarsLeft: CharCode.boxDrawingsDoubleVertical, 24 | verticalBarsRight: CharCode.boxDrawingsDoubleVertical, 25 | horizontalBarsTop: CharCode.boxDrawingsDoubleHorizontal, 26 | horizontalBarsBottom: CharCode.boxDrawingsDoubleHorizontal, 27 | }, 28 | "single-bar": { 29 | topLeftCorner: CharCode.boxDrawingsLightDownAndRight, 30 | topRightCorner: CharCode.boxDrawingsLightDownAndLeft, 31 | bottomLeftCorner: CharCode.boxDrawingsLightUpAndRight, 32 | bottomRightCorner: CharCode.boxDrawingsLightUpAndLeft, 33 | verticalBarsLeft: CharCode.boxDrawingsLightVertical, 34 | verticalBarsRight: CharCode.boxDrawingsLightVertical, 35 | horizontalBarsTop: CharCode.boxDrawingsLightHorizontal, 36 | horizontalBarsBottom: CharCode.boxDrawingsLightHorizontal, 37 | }, 38 | }; 39 | 40 | export function drawBorder(borderOptions: { 41 | terminal: BaseTerminal; 42 | foreColor?: Color; 43 | backColor?: Color; 44 | bounds: Rect; 45 | style: "double-bar" | "single-bar"; 46 | }) { 47 | const { terminal, foreColor, backColor, bounds, style } = borderOptions; 48 | 49 | const topLeftCorner = bounds.v1; 50 | const topRightCorner = { x: bounds.v2.x, y: bounds.v1.y }; 51 | const bottomLeftCorner = { x: bounds.v1.x, y: bounds.v2.y }; 52 | const bottomRightCorner = bounds.v2; 53 | 54 | const tiles = tileMap[style]; 55 | 56 | // Corners 57 | terminal.drawGlyph( 58 | topLeftCorner, 59 | Glyph.fromCharCode(tiles.topLeftCorner, foreColor, backColor) 60 | ); 61 | terminal.drawGlyph( 62 | topRightCorner, 63 | Glyph.fromCharCode(tiles.topRightCorner, foreColor, backColor) 64 | ); 65 | terminal.drawGlyph( 66 | bottomLeftCorner, 67 | Glyph.fromCharCode(tiles.bottomLeftCorner, foreColor, backColor) 68 | ); 69 | terminal.drawGlyph( 70 | bottomRightCorner, 71 | Glyph.fromCharCode(tiles.bottomRightCorner, foreColor, backColor) 72 | ); 73 | 74 | // Horizontal Bars 75 | for (let dx = topLeftCorner.x + 1; dx < topRightCorner.x; dx++) { 76 | terminal.drawGlyph( 77 | { x: dx, y: topLeftCorner.y }, 78 | Glyph.fromCharCode(tiles.horizontalBarsTop, foreColor, backColor) 79 | ); 80 | terminal.drawGlyph( 81 | { x: dx, y: bottomRightCorner.y }, 82 | Glyph.fromCharCode(tiles.horizontalBarsBottom, foreColor, backColor) 83 | ); 84 | } 85 | 86 | // Vertical Bars 87 | for (let dy = topLeftCorner.y + 1; dy < bottomLeftCorner.y; dy++) { 88 | terminal.drawGlyph( 89 | { x: topLeftCorner.x, y: dy }, 90 | Glyph.fromCharCode(tiles.verticalBarsLeft, foreColor, backColor) 91 | ); 92 | terminal.drawGlyph( 93 | { x: topRightCorner.x, y: dy }, 94 | Glyph.fromCharCode(tiles.verticalBarsRight, foreColor, backColor) 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/input/index.ts: -------------------------------------------------------------------------------- 1 | export { KeyCode } from "./keycode"; 2 | export { 3 | KeyboardContext, 4 | KeyboardHandler, 5 | KeyboardHandlerEvent, 6 | KeyboardContextCallback, 7 | } from "./keyboard"; 8 | export { 9 | MouseHandler, 10 | MouseContext, 11 | MouseHandlerEvent, 12 | MouseContextCallback, 13 | } from "./mouse"; 14 | -------------------------------------------------------------------------------- /src/input/keycode.ts: -------------------------------------------------------------------------------- 1 | export enum KeyCode { 2 | Backspace = 8, 3 | Tab = 9, 4 | Enter = 13, 5 | Shift = 16, 6 | Ctrl = 17, 7 | Alt = 18, 8 | PauseBreak = 19, 9 | CapsLock = 20, 10 | Escape = 27, 11 | Space = 32, 12 | PageUp = 33, 13 | PageDown = 34, 14 | End = 35, 15 | Home = 36, 16 | 17 | LeftArrow = 37, 18 | UpArrow = 38, 19 | RightArrow = 39, 20 | DownArrow = 40, 21 | 22 | Insert = 45, 23 | Delete = 46, 24 | 25 | Zero = 48, 26 | ClosedParen = Zero, 27 | One = 49, 28 | ExclamationMark = One, 29 | Two = 50, 30 | AtSign = Two, 31 | Three = 51, 32 | PoundSign = Three, 33 | Hash = PoundSign, 34 | Four = 52, 35 | DollarSign = Four, 36 | Five = 53, 37 | PercentSign = Five, 38 | Six = 54, 39 | Caret = Six, 40 | Hat = Caret, 41 | Seven = 55, 42 | Ampersand = Seven, 43 | Eight = 56, 44 | Star = Eight, 45 | Asterik = Star, 46 | Nine = 57, 47 | OpenParen = Nine, 48 | 49 | A = 65, 50 | B = 66, 51 | C = 67, 52 | D = 68, 53 | E = 69, 54 | F = 70, 55 | G = 71, 56 | H = 72, 57 | I = 73, 58 | J = 74, 59 | K = 75, 60 | L = 76, 61 | M = 77, 62 | N = 78, 63 | O = 79, 64 | P = 80, 65 | Q = 81, 66 | R = 82, 67 | S = 83, 68 | T = 84, 69 | U = 85, 70 | V = 86, 71 | W = 87, 72 | X = 88, 73 | Y = 89, 74 | Z = 90, 75 | 76 | LeftWindowKey = 91, 77 | RightWindowKey = 92, 78 | SelectKey = 93, 79 | 80 | Numpad0 = 96, 81 | Numpad1 = 97, 82 | Numpad2 = 98, 83 | Numpad3 = 99, 84 | Numpad4 = 100, 85 | Numpad5 = 101, 86 | Numpad6 = 102, 87 | Numpad7 = 103, 88 | Numpad8 = 104, 89 | Numpad9 = 105, 90 | 91 | Multiply = 106, 92 | Add = 107, 93 | Subtract = 109, 94 | DecimalPoint = 110, 95 | Divide = 111, 96 | 97 | F1 = 112, 98 | F2 = 113, 99 | F3 = 114, 100 | F4 = 115, 101 | F5 = 116, 102 | F6 = 117, 103 | F7 = 118, 104 | F8 = 119, 105 | F9 = 120, 106 | F10 = 121, 107 | F11 = 122, 108 | F12 = 123, 109 | 110 | NumLock = 144, 111 | ScrollLock = 145, 112 | 113 | SemiColon = 186, 114 | Equals = 187, 115 | Comma = 188, 116 | Dash = 189, 117 | Period = 190, 118 | UnderScore = Dash, 119 | PlusSign = Equals, 120 | ForwardSlash = 191, 121 | Tilde = 192, 122 | GraveAccent = Tilde, 123 | 124 | OpenBracket = 219, 125 | ClosedBracket = 221, 126 | Quote = 222, 127 | } 128 | -------------------------------------------------------------------------------- /src/input/test-utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { JSDOM } from "jsdom"; 2 | 3 | export function setupTestDom() { 4 | const dom = new JSDOM(); 5 | //@ts-ignore 6 | global.document = dom.window.document; 7 | //@ts-ignore 8 | global.window = dom.window; 9 | //@ts-ignore 10 | global.Image = window.Image; 11 | //@ts-ignore 12 | global.MouseEvent = window.MouseEvent; 13 | 14 | //@ts-ignore 15 | window.HTMLCanvasElement.prototype.getContext = function () { 16 | return { 17 | fillRect: function () {}, 18 | clearRect: function () {}, 19 | getImageData: function (x: number, y: number, w: number, h: number) { 20 | return { 21 | data: new Array(w * h * 4), 22 | }; 23 | }, 24 | putImageData: function () {}, 25 | createImageData: function () { 26 | return []; 27 | }, 28 | setTransform: function () {}, 29 | drawImage: function () {}, 30 | save: function () {}, 31 | fillText: function () {}, 32 | restore: function () {}, 33 | beginPath: function () {}, 34 | moveTo: function () {}, 35 | lineTo: function () {}, 36 | closePath: function () {}, 37 | stroke: function () {}, 38 | translate: function () {}, 39 | scale: function () {}, 40 | rotate: function () {}, 41 | arc: function () {}, 42 | fill: function () {}, 43 | measureText: function () { 44 | return { width: 0 }; 45 | }, 46 | transform: function () {}, 47 | rect: function () {}, 48 | clip: function () {}, 49 | }; 50 | }; 51 | } 52 | 53 | describe("setupTestDom", () => { 54 | it("won't error on setup", () => { 55 | expect(() => setupTestDom()).not.toThrow(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/malwoden.spec.ts: -------------------------------------------------------------------------------- 1 | import * as Malwoden from "./malwoden"; 2 | 3 | describe("lib", () => { 4 | it("has expected modules", () => { 5 | expect(Malwoden.CharCode).toBeTruthy(); 6 | expect(Malwoden.Color).toBeTruthy(); 7 | expect(Malwoden.Calc).toBeTruthy(); 8 | expect(Malwoden.FOV).toBeTruthy(); 9 | expect(Malwoden.Generation).toBeTruthy(); 10 | expect(Malwoden.Glyph).toBeTruthy(); 11 | expect(Malwoden.GUI).toBeTruthy(); 12 | expect(Malwoden.Rand).toBeTruthy(); 13 | expect(Malwoden.Input).toBeTruthy(); 14 | expect(Malwoden.Terminal).toBeTruthy(); 15 | expect(Malwoden.Struct).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/malwoden.ts: -------------------------------------------------------------------------------- 1 | export * as Terminal from "./terminal"; 2 | export { CharCode, Glyph, Color } from "./terminal"; 3 | export * as Input from "./input"; 4 | export * as Generation from "./generation"; 5 | export * as Pathfinding from "./pathfinding"; 6 | export * as Calc from "./calc"; 7 | export * as GUI from "./gui"; 8 | export * as Rand from "./rand"; 9 | export * as Struct from "./struct"; 10 | export * as FOV from "./fov"; 11 | export { Vector2, Rect } from "./struct"; 12 | -------------------------------------------------------------------------------- /src/pathfinding/index.ts: -------------------------------------------------------------------------------- 1 | export { AStar } from "./astar"; 2 | export { Dijkstra } from "./dijkstra"; 3 | export { RangeFinder } from "./range-finder"; 4 | export { RangeVector2 } from "./pathfinding-common"; 5 | -------------------------------------------------------------------------------- /src/pathfinding/pathfinding-common.ts: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "../struct"; 2 | 3 | /** 4 | * A function used to tell if a space is blocked. 5 | * Can be a bit more intuitive than using a distance 6 | * callback with Infinity values. 7 | */ 8 | export interface IsBlockedCallback { 9 | (v: Vector2): boolean; 10 | } 11 | 12 | /** 13 | * A function that returns the terrain cost. The 'from' parameter 14 | * can usually be ignored, unless the starting terrain factors in. 15 | */ 16 | export interface TerrainCallback { 17 | (from: Vector2, to: Vector2): number; 18 | } 19 | 20 | /** A Vector2 that includes a range from an origin point */ 21 | export interface RangeVector2 { 22 | /** The x coordinate */ 23 | x: number; 24 | /** The y coordinate */ 25 | y: number; 26 | /** The range */ 27 | r: number; 28 | } 29 | -------------------------------------------------------------------------------- /src/pathfinding/range-finder.ts: -------------------------------------------------------------------------------- 1 | import { getRing4 } from "../fov/get-ring"; 2 | import { Vector2, HeapPriorityQueue } from "../struct"; 3 | import { TerrainCallback, RangeVector2 } from "./pathfinding-common"; 4 | 5 | /** 6 | * Used to find a range from a central point, like movement or ranged attacks in 7 | * turn-based games. This uses a search similar to Dijkstra or AStar, but returns 8 | * a list of tiles in range of a starting point, rather than a path to a goal. 9 | */ 10 | export class RangeFinder { 11 | private getTerrain: TerrainCallback = () => 1; 12 | readonly topology: "four" | "eight"; 13 | 14 | /** 15 | * @param config - Configuration for the RangeFinder 16 | * @param config.topology - four | eight 17 | * @param config.getTerrainCallback - Override the distance function for terrain costs or blocked spaces. 18 | */ 19 | constructor(config: { 20 | getTerrainCallback?: TerrainCallback; 21 | topology: "four" | "eight"; 22 | }) { 23 | this.topology = config.topology; 24 | if (config.getTerrainCallback) { 25 | this.getTerrain = config.getTerrainCallback; 26 | } 27 | } 28 | 29 | private getNeighbors(pos: Vector2): Vector2[] { 30 | const neighbors = getRing4(pos.x, pos.y, 1); 31 | 32 | if (this.topology === "eight") { 33 | neighbors.push({ x: pos.x + 1, y: pos.y - 1 }); 34 | neighbors.push({ x: pos.x - 1, y: pos.y - 1 }); 35 | neighbors.push({ x: pos.x - 1, y: pos.y + 1 }); 36 | neighbors.push({ x: pos.x + 1, y: pos.y + 1 }); 37 | } 38 | 39 | return neighbors; 40 | } 41 | 42 | /** 43 | * Find the range from a given point. 44 | * @param config - Configuration for the findRange 45 | * @param config.start - Vector2 - The starting point 46 | * @param config.maxRange - The maximum range allowed 47 | * @param config.minRange - The minimum range allowed (optional) 48 | * @returns - RangeVector2[] ({x,y,r}[]) 49 | */ 50 | compute(config: { 51 | start: Vector2; 52 | maxRange: number; 53 | minRange?: number; 54 | }): RangeVector2[] { 55 | const { start, maxRange: range, minRange = 0 } = config; 56 | 57 | // Nodes we will process neighbors of 58 | const horizon = new HeapPriorityQueue((v) => v.r); 59 | horizon.insert({ ...start, r: 0 }); 60 | 61 | const explored = new Map(); 62 | explored.set(`${start.x}:${start.y}`, 0); 63 | 64 | while (horizon.size()) { 65 | // Handle current node first 66 | const current = horizon.pop()!; 67 | 68 | // Handle neighbors 69 | const neighbors = this.getNeighbors(current); 70 | for (let n of neighbors) { 71 | const distance = current.r + this.getTerrain(current, n); 72 | const neighbor = { ...n, r: distance }; 73 | 74 | // See if it's even in range 75 | if (neighbor.r > range) continue; 76 | 77 | // We pay attention only if we haven't seen it before, 78 | // or we just found a shorter path 79 | const importantNeighbor = 80 | !explored.has(`${neighbor.x}:${neighbor.y}`) || 81 | neighbor.r < explored.get(`${neighbor.x}:${neighbor.y}`)!; 82 | 83 | if (importantNeighbor) { 84 | explored.set(`${neighbor.x}:${neighbor.y}`, neighbor.r); 85 | horizon.insert(neighbor); 86 | } 87 | } 88 | } 89 | 90 | // Return everything in range 91 | const result: RangeVector2[] = []; 92 | explored.forEach((distance, vs) => { 93 | // Return if not in the right range 94 | if (distance < minRange || distance > range) return; 95 | 96 | const [x, y] = vs.split(":"); 97 | result.push({ 98 | x: Number.parseInt(x), 99 | y: Number.parseInt(y), 100 | r: distance, 101 | }); 102 | }); 103 | return result; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/rand/alea.spec.ts: -------------------------------------------------------------------------------- 1 | import { AleaRNG } from "./alea"; 2 | 3 | describe("AleaRNG", () => { 4 | it("Can generate numbers", () => { 5 | const aa = new AleaRNG("hello"); 6 | const bb = new AleaRNG("hello"); 7 | const cc = aa.clone(); 8 | 9 | // Run them in sync to compare 10 | for (let i = 0; i < 100; i++) { 11 | const a = aa.next(); 12 | const b = bb.next(); 13 | const c = cc.next(); 14 | 15 | expect(a).toEqual(b); 16 | expect(b).toEqual(c); 17 | } 18 | 19 | // Ints 20 | for (let i = 0; i < 100; i++) { 21 | const a = aa.nextInt(); 22 | const b = bb.nextInt(); 23 | const c = cc.nextInt(); 24 | 25 | expect(a).toEqual(b); 26 | expect(b).toEqual(c); 27 | } 28 | }); 29 | 30 | it("Will generate different numbers with different seeds", () => { 31 | const aa = new AleaRNG(Math.random().toString()); 32 | const bb = new AleaRNG(Math.random().toString()); 33 | 34 | const a = []; 35 | const b = []; 36 | 37 | for (let i = 0; i < 100; i++) { 38 | a.push(aa.next()); 39 | b.push(bb.next()); 40 | } 41 | 42 | expect(a).not.toEqual(b); 43 | }); 44 | 45 | it("will generate numbers in the correct ranges", () => { 46 | const aa = new AleaRNG(); 47 | 48 | // ints 49 | const min = 50; 50 | const max = 55; 51 | for (let i = 0; i < 1000; i++) { 52 | const v = aa.nextInt(min, max); 53 | const v2 = aa.next(min, max); 54 | 55 | // Test int 56 | expect(v).toBeGreaterThanOrEqual(min); 57 | expect(v).toBeLessThan(max); 58 | 59 | // Test float 60 | expect(v2).toBeGreaterThanOrEqual(min); 61 | expect(v2).toBeLessThan(max); 62 | } 63 | }); 64 | 65 | it("Can reset itself", () => { 66 | const aa = new AleaRNG("hello"); 67 | const bb = new AleaRNG("hello"); 68 | 69 | for (let i = 0; i < 10; i++) { 70 | aa.next(); 71 | } 72 | 73 | aa.reset(); 74 | 75 | for (let i = 0; i < 100; i++) { 76 | expect(aa.next()).toEqual(bb.next()); 77 | } 78 | }); 79 | 80 | it("Can get booleans", () => { 81 | const aa = new AleaRNG(); 82 | const bools = []; 83 | 84 | for (let i = 0; i < 100; i++) { 85 | bools.push(aa.nextBoolean()); 86 | } 87 | 88 | expect(bools.some((x) => x)).toBeTruthy(); 89 | expect(bools.some((x) => !x)).toBeTruthy(); 90 | }); 91 | 92 | it("Can get a random item from an array", () => { 93 | const aa = new AleaRNG(); 94 | const nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 95 | 96 | for (let i = 0; i < 100; i++) { 97 | let e = aa.nextItem(nums); 98 | expect(e).not.toBeUndefined(); 99 | expect(nums.includes(e!)).toBeTruthy(); 100 | } 101 | 102 | expect(nums).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); 103 | 104 | expect(aa.nextItem([])).toEqual(undefined); 105 | }); 106 | 107 | it("Can shuffle an array", () => { 108 | const aa = new AleaRNG(); 109 | const alph = "abcdefghijklmnopqrstuvwxyz".split(""); 110 | 111 | const alph2 = aa.shuffle(alph); 112 | 113 | expect(alph).toEqual("abcdefghijklmnopqrstuvwxyz".split("")); 114 | expect(alph).not.toEqual(alph2); 115 | expect(alph).toHaveLength(alph2.length); 116 | 117 | for (let c of alph) { 118 | expect(alph2.indexOf(c)).toBeGreaterThanOrEqual(0); 119 | } 120 | }); 121 | 122 | it("Will sanitize the initial mash function 0 < x < 1", () => { 123 | const a = new AleaRNG(); 124 | a["s0"] = -0.5; 125 | a["s1"] = -0.2; 126 | a["s2"] = -0.1; 127 | 128 | a["sanitize"](); 129 | 130 | expect(a["s0"]).toEqual(0.5); 131 | expect(a["s1"]).toEqual(0.8); 132 | expect(a["s2"]).toEqual(0.9); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /src/rand/alea.ts: -------------------------------------------------------------------------------- 1 | import { IRNG } from "./rng"; 2 | 3 | function Mash() { 4 | var n = 0xefc8249d; 5 | 6 | var mash = function (data: string) { 7 | data = data.toString(); 8 | for (var i = 0; i < data.length; i++) { 9 | n += data.charCodeAt(i); 10 | var h = 0.02519603282416938 * n; 11 | n = h >>> 0; 12 | h -= n; 13 | h *= n; 14 | n = h >>> 0; 15 | h -= n; 16 | n += h * 0x100000000; // 2^32 17 | } 18 | return (n >>> 0) * 2.3283064365386963e-10; // 2^-32 19 | }; 20 | 21 | return mash; 22 | } 23 | 24 | /** 25 | * AleaRNG is an implementation based on 26 | * https://github.com/nquinlan/better-random-numbers-for-javascript-mirror 27 | * Johannes Baagøe , 2010 28 | */ 29 | export class AleaRNG implements IRNG { 30 | private seed: string; 31 | private s0 = 0; 32 | private s1 = 0; 33 | private s2 = 0; 34 | private c = 0; 35 | 36 | /** 37 | * Creates a new AleaRNG 38 | * @param seed - An optional string to seed the generation. Defaults to new Date() if not provided. 39 | */ 40 | constructor(seed?: string) { 41 | // Initialize seed if needed 42 | this.seed = seed === undefined ? new Date().toString() : seed; 43 | this.reset(); 44 | } 45 | 46 | /** 47 | * Resets the RNG to the original seed 48 | */ 49 | reset() { 50 | let mash = Mash(); 51 | 52 | // Initial mashes 53 | this.s0 = mash(" "); 54 | this.s1 = mash(" "); 55 | this.s2 = mash(" "); 56 | this.c = 1; 57 | 58 | this.s0 -= mash(this.seed); 59 | this.s1 -= mash(this.seed); 60 | this.s2 -= mash(this.seed); 61 | 62 | this.sanitize(); 63 | } 64 | 65 | private sanitize() { 66 | if (this.s0 < 0) this.s0 += 1; 67 | if (this.s1 < 0) this.s1 += 1; 68 | if (this.s2 < 0) this.s2 += 1; 69 | } 70 | 71 | private step(): number { 72 | // The dark magic that makes the number generator run 73 | var t = 2091639 * this.s0 + this.c * 2.3283064365386963e-10; // 2^-32 74 | this.s0 = this.s1; 75 | this.s1 = this.s2; 76 | return (this.s2 = t - (this.c = t | 0)); 77 | } 78 | 79 | // Returns between [0,1) 80 | private nextRand(): number { 81 | return ( 82 | this.step() + ((this.step() * 0x200000) | 0) * 1.1102230246251565e-16 83 | ); // 2^-53 84 | } 85 | 86 | /** 87 | * Returns a number between [min, max). Use nextInt() for integers. 88 | * @param min - The min (inclusive). Default 0. 89 | * @param max - The max (exclusive). Default 1. 90 | * @returns - A float between [min, max) 91 | */ 92 | next(min = 0, max = 1): number { 93 | return this.nextRand() * (max - min) + min; 94 | } 95 | 96 | /** 97 | * Returns an integer between [min, max). Use next() for floats. 98 | * @param min - The min (inclusive). Default 0. 99 | * @param max - The max (exclusive). Default 100. 100 | * @returns - An integer between [min, max) 101 | */ 102 | nextInt(min = 0, max = 100): number { 103 | return Math.floor(this.next() * (max - min) + min); 104 | } 105 | 106 | /** 107 | * Returns a boolean. 108 | * @returns - Either true or false 109 | */ 110 | nextBoolean(): boolean { 111 | return this.nextRand() > 0.5; 112 | } 113 | 114 | /** 115 | * Returns a random item from the given array. This does *not* remove the item from the array, 116 | * and multiple calls with the same array may yield the same item. If looking to randomize an array, 117 | * use shuffle(). 118 | * @param array - An array of items. 119 | * @returns - A single item from the array. 120 | */ 121 | nextItem(array: T[]): T | undefined { 122 | if (array.length === 0) return undefined; 123 | const i = this.nextInt(0, array.length); 124 | return array[i]; 125 | } 126 | 127 | /** 128 | * Shuffles all values inside an array. Returns a copy, and does not edit the original. 129 | * @param array - An array of items 130 | * @returns - A clone of the original array with all values shuffled. 131 | */ 132 | shuffle(array: T[]): T[] { 133 | const result: T[] = []; 134 | const clone = array.slice(); 135 | while (clone.length) { 136 | const index = this.nextInt(0, clone.length); 137 | result.push(clone.splice(index, 1)[0]); 138 | } 139 | return result; 140 | } 141 | 142 | /** 143 | * Returns a copy of the AleaRNG, with the seed and current step value. 144 | * @returns - A copy of the AleaRNG 145 | */ 146 | clone(): AleaRNG { 147 | const a = new AleaRNG(); 148 | a.s0 = this.s0; 149 | a.s1 = this.s1; 150 | a.s2 = this.s2; 151 | a.c = this.c; 152 | a.seed = this.seed; 153 | return a; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/rand/index.ts: -------------------------------------------------------------------------------- 1 | export { AleaRNG } from "./alea"; 2 | export { IRNG } from "./rng"; 3 | -------------------------------------------------------------------------------- /src/rand/rng.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An interface for random number generators. 3 | * Any random number generator from this library will implement the following, 4 | * and this library can use any custom generator that implements the following. 5 | */ 6 | export interface IRNG { 7 | next(min?: number, max?: number): number; 8 | nextInt(min?: number, max?: number): number; 9 | nextBoolean(): boolean; 10 | nextItem(array: T[]): T | undefined; 11 | shuffle(array: T[]): T[]; 12 | reset(): void; 13 | } 14 | -------------------------------------------------------------------------------- /src/struct/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./vector"; 2 | export * from "./table"; 3 | export * from "./priority-queue-heap"; 4 | export * from "./priority-queue-array"; 5 | export * from "./rect"; 6 | -------------------------------------------------------------------------------- /src/struct/line.spec.ts: -------------------------------------------------------------------------------- 1 | import { Line } from "./line"; 2 | 3 | describe("line", () => { 4 | it("can create a new line", () => { 5 | const line = new Line({ x: 1, y: 1 }, { x: 2, y: 2 }); 6 | 7 | expect(line.v1).toEqual({ x: 1, y: 1 }); 8 | expect(line.v2).toEqual({ x: 2, y: 2 }); 9 | 10 | expect(line.getDeltaX()).toBe(1); 11 | expect(line.getDeltaY()).toBe(1); 12 | }); 13 | 14 | it("can set new points", () => { 15 | const line = new Line({ x: 1, y: 1 }, { x: 2, y: 2 }); 16 | 17 | expect(line.getDeltaX()).toBe(1); 18 | expect(line.getDeltaY()).toBe(1); 19 | 20 | line.v1 = { x: 3, y: 3 }; 21 | line.v2 = { x: 5, y: 5 }; 22 | 23 | expect(line.getDeltaX()).toBe(2); 24 | expect(line.getDeltaY()).toBe(2); 25 | }); 26 | 27 | it("can clone a line", () => { 28 | const line = new Line({ x: 1, y: 2 }, { x: 3, y: 4 }); 29 | const cloned = line.clone(); 30 | 31 | expect(line.v1).toEqual(cloned.v1); 32 | expect(line.v2).toEqual(cloned.v2); 33 | }); 34 | 35 | it("can calculate collinear points", () => { 36 | const line = new Line({ x: 1, y: 1 }, { x: 2, y: 2 }); 37 | 38 | // Valid test cases 39 | const collinearPoints = [ 40 | { x: 1, y: 1 }, 41 | { x: 2, y: 2 }, 42 | { x: 3, y: 3 }, 43 | ]; 44 | 45 | for (let v of collinearPoints) { 46 | expect(line.isCollinear(v.x, v.y)).toBeTruthy(); 47 | expect(line.isAboveOrCollinear(v.x, v.y)).toBeTruthy(); 48 | expect(line.isBelowOrCollinear(v.x, v.y)).toBeTruthy(); 49 | } 50 | 51 | for (let v of collinearPoints) { 52 | expect(line.isAbove(v.x, v.y)).toBeFalsy(); 53 | expect(line.isBelow(v.x, v.y)).toBeFalsy(); 54 | } 55 | }); 56 | 57 | it("can calculate collinear lines", () => { 58 | const lineA = new Line({ x: 0, y: 0 }, { x: 1, y: 1 }); 59 | const lineB = new Line({ x: 1, y: 1 }, { x: 3, y: 3 }); 60 | 61 | expect(lineA.isLineCollinear(lineB)).toBeTruthy(); 62 | }); 63 | 64 | it("can calculate points above", () => { 65 | const line = new Line({ x: 1, y: 1 }, { x: 2, y: 2 }); 66 | 67 | const abovePoints = [ 68 | { x: 2, y: 3 }, 69 | { x: 4, y: 100 }, 70 | { x: -4, y: -3 }, 71 | ]; 72 | for (let v of abovePoints) { 73 | expect(line.isBelow(v.x, v.y)).toBeTruthy(); 74 | expect(line.isBelowOrCollinear(v.x, v.y)).toBeTruthy(); 75 | 76 | expect(line.isAbove(v.x, v.y)).toBeFalsy(); 77 | expect(line.isAboveOrCollinear(v.x, v.y)).toBeFalsy(); 78 | expect(line.isCollinear(v.x, v.y)).toBeFalsy(); 79 | } 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/struct/line.ts: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "./vector"; 2 | 3 | export class Line { 4 | private _v1: Vector2; 5 | private _v2: Vector2; 6 | 7 | get v1() { 8 | return this._v1; 9 | } 10 | 11 | get v2() { 12 | return this._v2; 13 | } 14 | 15 | set v1(val) { 16 | this._v1 = val; 17 | this.calcDeltas(); 18 | } 19 | 20 | set v2(val) { 21 | this._v2 = val; 22 | this.calcDeltas(); 23 | } 24 | 25 | private dx: number = 0; 26 | private dy: number = 0; 27 | 28 | constructor(v1: Vector2, v2: Vector2) { 29 | this._v1 = v1; 30 | this._v2 = v2; 31 | 32 | this.calcDeltas(); 33 | } 34 | 35 | private calcDeltas() { 36 | this.dx = this._v2.x - this.v1.x; 37 | this.dy = this._v2.y - this.v1.y; 38 | } 39 | 40 | clone(): Line { 41 | return new Line({ ...this._v1 }, { ...this._v2 }); 42 | } 43 | 44 | getDeltaX() { 45 | return this.dx; 46 | } 47 | 48 | getDeltaY() { 49 | return this.dy; 50 | } 51 | 52 | isBelow(x: number, y: number) { 53 | return this.calculateRelativeSlope(x, y) > 0; 54 | } 55 | 56 | isBelowOrCollinear(x: number, y: number) { 57 | return this.calculateRelativeSlope(x, y) >= 0; 58 | } 59 | 60 | isAbove(x: number, y: number) { 61 | return this.calculateRelativeSlope(x, y) < 0; 62 | } 63 | 64 | isAboveOrCollinear(x: number, y: number) { 65 | return this.calculateRelativeSlope(x, y) <= 0; 66 | } 67 | 68 | isCollinear(x: number, y: number) { 69 | return this.calculateRelativeSlope(x, y) === 0; 70 | } 71 | 72 | isLineCollinear(line: Line) { 73 | return ( 74 | this.isCollinear(line.v1.x, line.v1.y) && 75 | this.isCollinear(line.v2.x, line.v2.y) 76 | ); 77 | } 78 | 79 | calculateRelativeSlope(x: number, y: number): number { 80 | return this.dy * (this.v2.x - x) - this.dx * (this.v2.y - y); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/struct/priority-queue-array.spec.ts: -------------------------------------------------------------------------------- 1 | import { ArrayPriorityQueue } from "./priority-queue-array"; 2 | import { Vector2 } from "./vector"; 3 | 4 | describe("PriorityQueue", () => { 5 | it("Can take in numbers", () => { 6 | const q = new ArrayPriorityQueue((n: number) => n); 7 | 8 | q.insert(1000); 9 | q.insert(100); 10 | q.insert(10); 11 | q.insert(1); 12 | 13 | expect(q.pop()).toEqual(1); 14 | expect(q.pop()).toEqual(10); 15 | expect(q.pop()).toEqual(100); 16 | expect(q.pop()).toEqual(1000); 17 | }); 18 | 19 | it("Can peek at the top entry", () => { 20 | const q = new ArrayPriorityQueue((n: number) => n); 21 | 22 | q.insert(100); 23 | q.insert(50); 24 | q.insert(500); 25 | 26 | expect(q.peek()).toEqual(50); 27 | expect(q.peek()).toEqual(50); 28 | expect(q.peek()).toEqual(50); 29 | }); 30 | 31 | it("Will return undefined when peeking at an empty queue", () => { 32 | const q = new ArrayPriorityQueue((n: number) => n); 33 | 34 | expect(q.peek()).toEqual(undefined); 35 | }); 36 | 37 | it("Will return undefined when popping an empty queue", () => { 38 | const q = new ArrayPriorityQueue((n: number) => n); 39 | 40 | expect(q.pop()).toEqual(undefined); 41 | }); 42 | 43 | it("Can take in objects", () => { 44 | const q = new ArrayPriorityQueue((v: Vector2) => v.x); 45 | 46 | q.insert({ x: 100, y: 10 }); 47 | q.insert({ x: 10, y: 10 }); 48 | q.insert({ x: 1, y: 10 }); 49 | q.insert({ x: -1, y: 10 }); 50 | q.insert({ x: -10, y: 10 }); 51 | q.insert({ x: -100, y: 10 }); 52 | 53 | expect(q.pop()).toEqual({ x: -100, y: 10 }); 54 | expect(q.pop()).toEqual({ x: -10, y: 10 }); 55 | expect(q.pop()).toEqual({ x: -1, y: 10 }); 56 | expect(q.pop()).toEqual({ x: 1, y: 10 }); 57 | expect(q.pop()).toEqual({ x: 10, y: 10 }); 58 | expect(q.pop()).toEqual({ x: 100, y: 10 }); 59 | }); 60 | 61 | it("Will prioritize object inserted first", () => { 62 | const q = new ArrayPriorityQueue((v: Vector2) => v.x); 63 | 64 | for (let y = 0; y < 100; y++) { 65 | q.insert({ x: 0, y }); 66 | } 67 | 68 | for (let y = 0; y < 100; y++) { 69 | expect(q.pop()).toEqual({ x: 0, y }); 70 | } 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/struct/priority-queue-array.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ArrayPriorityQueue allows for push/pop based on an attribute 3 | * of the inserted objects. The array based implementation 4 | * underneath has theoretic O(c) insert time, and O(n) 5 | * peek/pop time. 6 | * 7 | * Though this is slower in many cases than the heap based implementation, 8 | * it preserves the order better and can result in more 'normal' paths 9 | * when used with pathfinding. 10 | */ 11 | export class ArrayPriorityQueue { 12 | private data: [number, T][] = []; 13 | private priorityFunc: (t: T) => number; 14 | 15 | /** 16 | * @param priorityFunc - A function that takes the a value that 17 | * has previously been inserted, and returns a priority. 18 | * 19 | * A lower score is higher priority. 20 | * 21 | * Ex. (monster) => monster.speed 22 | */ 23 | constructor(priorityFunc: (t: T) => number) { 24 | this.priorityFunc = priorityFunc; 25 | } 26 | 27 | /** 28 | * Insert data into the queue 29 | * @param data - The data to insert 30 | */ 31 | insert(data: T) { 32 | const score = this.priorityFunc(data); 33 | this.data.push([score, data]); 34 | } 35 | 36 | /** 37 | * Get the item with the lowest priority score, 38 | * removing it from the queue. 39 | * @returns - The lowest priority item 40 | */ 41 | pop(): T | undefined { 42 | if (this.data.length === 0) return undefined; 43 | 44 | let min = Infinity; 45 | let minIndex = -1; 46 | this.data.forEach(([val, d], index) => { 47 | if (val < min) { 48 | min = val; 49 | minIndex = index; 50 | } 51 | }); 52 | 53 | const popped = this.data.splice(minIndex, 1); 54 | return popped[0][1]; 55 | } 56 | 57 | /** 58 | * Get the item with the lowest priotity score, 59 | * WITHOUT removing it. 60 | * @returns - The lowest priority item 61 | */ 62 | peek(): T | undefined { 63 | if (this.data.length === 0) return undefined; 64 | 65 | let min = Infinity; 66 | let minIndex = -1; 67 | this.data.forEach(([val, d], index) => { 68 | if (val < min) { 69 | min = val; 70 | minIndex = index; 71 | } 72 | }); 73 | 74 | return this.data[minIndex][1]; 75 | } 76 | 77 | /** 78 | * Returns the number of items in the queue. 79 | * @returns - Number of items in the queue. 80 | */ 81 | size(): number { 82 | return this.data.length; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/struct/priority-queue-heap.spec.ts: -------------------------------------------------------------------------------- 1 | import { HeapPriorityQueue } from "./priority-queue-heap"; 2 | import { Vector2 } from "./vector"; 3 | 4 | describe("PriorityQueue", () => { 5 | it("Can take in numbers", () => { 6 | const q = new HeapPriorityQueue((n: number) => n); 7 | 8 | q.insert(1000); 9 | q.insert(100); 10 | q.insert(10); 11 | q.insert(1); 12 | 13 | expect(q.pop()).toEqual(1); 14 | expect(q.pop()).toEqual(10); 15 | expect(q.pop()).toEqual(100); 16 | expect(q.pop()).toEqual(1000); 17 | }); 18 | 19 | it("Can peek at the top entry", () => { 20 | const q = new HeapPriorityQueue((n: number) => n); 21 | 22 | q.insert(100); 23 | q.insert(50); 24 | q.insert(500); 25 | 26 | expect(q.peek()).toEqual(50); 27 | expect(q.peek()).toEqual(50); 28 | expect(q.peek()).toEqual(50); 29 | }); 30 | 31 | it("Will return undefined when peeking at an empty queue", () => { 32 | const q = new HeapPriorityQueue((n: number) => n); 33 | 34 | expect(q.peek()).toEqual(undefined); 35 | }); 36 | 37 | it("Will return undefined when popping an empty queue", () => { 38 | const q = new HeapPriorityQueue((n: number) => n); 39 | 40 | expect(q.pop()).toEqual(undefined); 41 | }); 42 | 43 | it("Can take in objects", () => { 44 | const q = new HeapPriorityQueue((v: Vector2) => v.x); 45 | 46 | q.insert({ x: 100, y: 10 }); 47 | q.insert({ x: 10, y: 10 }); 48 | q.insert({ x: 1, y: 10 }); 49 | q.insert({ x: -1, y: 10 }); 50 | q.insert({ x: -10, y: 10 }); 51 | q.insert({ x: -100, y: 10 }); 52 | 53 | expect(q.pop()).toEqual({ x: -100, y: 10 }); 54 | expect(q.pop()).toEqual({ x: -10, y: 10 }); 55 | expect(q.pop()).toEqual({ x: -1, y: 10 }); 56 | expect(q.pop()).toEqual({ x: 1, y: 10 }); 57 | expect(q.pop()).toEqual({ x: 10, y: 10 }); 58 | expect(q.pop()).toEqual({ x: 100, y: 10 }); 59 | }); 60 | 61 | it("Will prioritize object inserted first", () => { 62 | const q = new HeapPriorityQueue((v: Vector2) => v.x); 63 | 64 | for (let y = 0; y < 100; y++) { 65 | q.insert({ x: 0, y }); 66 | } 67 | 68 | for (let y = 0; y < 100; y++) { 69 | expect(q.pop()).toEqual({ x: 0, y }); 70 | } 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/struct/priority-queue-heap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * HeapPriorityQueue allows for push/pop based on an attribute 3 | * of the inserted objects. The heap based implementation 4 | * underneath has theoretic O(log(n)) insert/pop time and O(c) 5 | * peek time. 6 | * 7 | * Because the bubble up/down might not preserve order within 8 | * items of the same priority, the array-priority-queue can be 9 | * a better though slower choice in some cases. 10 | */ 11 | export class HeapPriorityQueue { 12 | private heap: T[] = []; 13 | private priorityFunc: (t: T) => number; 14 | 15 | /** 16 | * @param priorityFunc - A function that takes the a value that 17 | * has previously been inserted, and returns a priority. 18 | * 19 | * A lower score is higher priority. 20 | * 21 | * Ex. (monster) => monster.speed 22 | */ 23 | constructor(priorityFunc: (t: T) => number) { 24 | this.priorityFunc = priorityFunc; 25 | } 26 | 27 | private getPriority(t: T): number { 28 | return this.priorityFunc(t); 29 | } 30 | 31 | private findParent(index: number) { 32 | if (index % 2) { 33 | return (index - 1) / 2; 34 | } else { 35 | return Math.ceil((index - 1) / 2); 36 | } 37 | } 38 | 39 | private getChildFunction(parentIdx: number) { 40 | let [left, right] = [2 * parentIdx + 1, 2 * parentIdx + 2]; 41 | 42 | if (left >= this.heap.length - 1) { 43 | // cannot go out of bounds for the array. 44 | return Infinity; 45 | } 46 | 47 | if ( 48 | this.getPriority(this.heap[left]) < this.getPriority(this.heap[right]) 49 | ) { 50 | return left; 51 | } else { 52 | return right; 53 | } 54 | } 55 | 56 | private bubbleUpwards() { 57 | let currentNodeIdx = this.heap.length - 1; 58 | let currentNodeParentIdx = this.findParent(currentNodeIdx); 59 | const newNode = this.heap[currentNodeIdx]; 60 | 61 | while ( 62 | this.getPriority(newNode) < 63 | this.getPriority(this.heap[currentNodeParentIdx]) 64 | ) { 65 | const parent = this.heap[currentNodeParentIdx]; 66 | this.heap[currentNodeParentIdx] = newNode; 67 | this.heap[currentNodeIdx] = parent; 68 | currentNodeIdx = currentNodeParentIdx; 69 | currentNodeParentIdx = Math.floor(currentNodeIdx / 2); 70 | } 71 | } 72 | 73 | private bubbleDownwards() { 74 | // Special case for smaller sizes 75 | if (this.size() < 3) { 76 | this.heap.sort((a, b) => this.getPriority(a) - this.getPriority(b)); 77 | return; 78 | } 79 | 80 | let currentIdx = 0; 81 | let currentChildIdx: number = this.getChildFunction(currentIdx); 82 | 83 | while ( 84 | this.getPriority(this.heap[currentIdx]) > 85 | this.getPriority(this.heap[currentChildIdx]) 86 | ) { 87 | const futureIdx = currentChildIdx; // this will become the new parent node at the end. 88 | const currentNode = this.heap[currentIdx]; // grabs the node. 89 | const futureNode = this.heap[currentChildIdx]; // grabs the child node. 90 | this.heap[currentChildIdx] = currentNode; // swaps node 91 | this.heap[currentIdx] = futureNode; // swaps node 92 | currentIdx = futureIdx; // sets the current node from the child 93 | currentChildIdx = this.getChildFunction(currentIdx); // gets children. 94 | if (currentChildIdx === Infinity) { 95 | break; 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * Insert data into the queue 102 | * @param data - The data to insert 103 | */ 104 | insert(data: T) { 105 | this.heap.push(data); 106 | this.bubbleUpwards(); 107 | } 108 | 109 | /** 110 | * Get the item with the lowest priority score, 111 | * removing it from the queue. 112 | * @returns - The lowest priority item 113 | */ 114 | pop() { 115 | const toRemove = this.heap.shift(); 116 | this.bubbleDownwards(); 117 | return toRemove; 118 | } 119 | 120 | /** 121 | * Get the item with the lowest priotity score, 122 | * WITHOUT removing it. 123 | * @returns - The lowest priority item 124 | */ 125 | peek() { 126 | return this.heap[0]; 127 | } 128 | 129 | /** 130 | * Returns the number of items in the queue. 131 | * @returns - Number of items in the queue. 132 | */ 133 | size(): number { 134 | return this.heap.length; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/struct/rect.spec.ts: -------------------------------------------------------------------------------- 1 | import { Rect } from "./rect"; 2 | 3 | describe("rect", () => { 4 | it("Can create a basic rectangle", () => { 5 | const rect = new Rect({ x: 0, y: 0 }, { x: 5, y: 10 }); 6 | 7 | expect(rect.v1).toEqual({ x: 0, y: 0 }); 8 | expect(rect.v2).toEqual({ x: 5, y: 10 }); 9 | }); 10 | 11 | it("Will remap v1/v2 to be the min/max corners", () => { 12 | const rect = new Rect({ x: 5, y: 10 }, { x: -5, y: -10 }); 13 | expect(rect.v1).toEqual({ x: -5, y: -10 }); 14 | expect(rect.v2).toEqual({ x: 5, y: 10 }); 15 | 16 | const rect2 = new Rect({ x: 5, y: -10 }, { x: -5, y: 10 }); 17 | expect(rect2.v1).toEqual({ x: -5, y: -10 }); 18 | expect(rect2.v2).toEqual({ x: 5, y: 10 }); 19 | }); 20 | 21 | it("Can get width and height", () => { 22 | const rect = new Rect({ x: 0, y: 0 }, { x: 5, y: 10 }); 23 | 24 | expect(rect.width()).toEqual(6); 25 | expect(rect.height()).toEqual(11); 26 | }); 27 | 28 | it("Can check if a rectangle intersects", () => { 29 | const tests: [Rect, Rect, Boolean][] = [ 30 | [ 31 | new Rect({ x: 0, y: 0 }, { x: 5, y: 10 }), 32 | new Rect({ x: 0, y: 0 }, { x: 5, y: 10 }), 33 | true, 34 | ], 35 | [ 36 | new Rect({ x: 0, y: 0 }, { x: 5, y: 10 }), 37 | new Rect({ x: -5, y: -5 }, { x: 0, y: 0 }), 38 | true, 39 | ], 40 | [ 41 | new Rect({ x: 0, y: 0 }, { x: 5, y: 10 }), 42 | new Rect({ x: -5, y: -5 }, { x: -1, y: -1 }), 43 | false, 44 | ], 45 | [ 46 | new Rect({ x: 0, y: 0 }, { x: 5, y: 10 }), 47 | new Rect({ x: 5, y: -5 }, { x: 5, y: -1 }), 48 | false, 49 | ], 50 | [ 51 | new Rect({ x: 0, y: 0 }, { x: 5, y: 10 }), 52 | new Rect({ x: 5, y: 10 }, { x: 6, y: 11 }), 53 | true, 54 | ], 55 | [ 56 | new Rect({ x: 0, y: 0 }, { x: 5, y: 10 }), 57 | new Rect({ x: 6, y: 11 }, { x: 6, y: 11 }), 58 | false, 59 | ], 60 | ]; 61 | 62 | for (let [r1, r2, bool] of tests) { 63 | expect(r1.intersects(r2)).toEqual(bool); 64 | expect(r2.intersects(r1)).toEqual(bool); 65 | } 66 | }); 67 | 68 | it("can get a center", () => { 69 | const rectEven = new Rect({ x: 0, y: 0 }, { x: 5, y: 5 }); 70 | const rectOdd = new Rect({ x: 0, y: 0 }, { x: 4, y: 4 }); 71 | 72 | expect(rectOdd.center()).toEqual({ x: 2, y: 2 }); 73 | expect(rectEven.center()).toEqual({ x: 2, y: 2 }); 74 | }); 75 | 76 | it("can create a rect from width/height", () => { 77 | const r = Rect.FromWidthHeight({ x: 3, y: 5 }, 2, 5); 78 | expect(r.v1).toEqual({ x: 3, y: 5 }); 79 | expect(r.v2).toEqual({ x: 4, y: 9 }); 80 | }); 81 | 82 | it("will throw an error on invalid width/height", () => { 83 | expect(() => Rect.FromWidthHeight({ x: 0, y: 0 }, 0, 0)).toThrow(); 84 | expect(() => Rect.FromWidthHeight({ x: 0, y: 0 }, 0, 1)).toThrow(); 85 | expect(() => Rect.FromWidthHeight({ x: 0, y: 0 }, 1, 0)).toThrow(); 86 | }); 87 | 88 | it("can detect if it contains a point", () => { 89 | const r = new Rect({ x: 0, y: 0 }, { x: 10, y: 10 }); 90 | 91 | expect(r.contains({ x: -1, y: 0 })).toBeFalsy(); 92 | expect(r.contains({ x: 0, y: 0 })).toBeTruthy(); 93 | expect(r.contains({ x: 10, y: 0 })).toBeTruthy(); 94 | expect(r.contains({ x: 11, y: 0 })).toBeFalsy(); 95 | 96 | expect(r.contains({ x: 5, y: -1 })).toBeFalsy(); 97 | expect(r.contains({ x: 5, y: 0 })).toBeTruthy(); 98 | expect(r.contains({ x: 5, y: 10 })).toBeTruthy(); 99 | expect(r.contains({ x: 5, y: 11 })).toBeFalsy(); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/struct/rect.ts: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "./vector"; 2 | 3 | /** Represents a basic rectangle. */ 4 | export class Rect { 5 | readonly v1: Vector2; 6 | readonly v2: Vector2; 7 | 8 | /** 9 | * Creates a rect from width + height. The width x height of a 1x1 block 10 | * would be 1, with v2 equal to v1. 11 | * @param v1 Vector2 - The starting vector 12 | * @param width number - The width. Min 1 13 | * @param height number - The height - Min 1 14 | * @returns Rect - A new rect with v2 computed from width/height 15 | */ 16 | static FromWidthHeight(v1: Vector2, width: number, height: number): Rect { 17 | if (width < 1) { 18 | throw new Error("Rect width must be at least 1."); 19 | } 20 | if (height < 1) { 21 | throw new Error("Rect height must be at least 1."); 22 | } 23 | const v2 = { x: v1.x + width - 1, y: v1.y + height - 1 }; 24 | return new Rect(v1, v2); 25 | } 26 | 27 | /** 28 | * Creates a rectangle. Will internally set v1 to the min 29 | * coordinate corder, and v2 to the max corner. 30 | * 31 | * @param v1 - A vector representing one of the corners 32 | * @param v2 - A vector representing the other corner 33 | */ 34 | constructor(v1: Vector2, v2: Vector2) { 35 | this.v1 = { 36 | x: Math.min(v1.x, v2.x), 37 | y: Math.min(v1.y, v2.y), 38 | }; 39 | this.v2 = { 40 | x: Math.max(v1.x, v2.x), 41 | y: Math.max(v1.y, v2.y), 42 | }; 43 | } 44 | 45 | width(): number { 46 | return Math.abs(this.v2.x - this.v1.x) + 1; 47 | } 48 | 49 | height(): number { 50 | return Math.abs(this.v2.y - this.v1.y) + 1; 51 | } 52 | 53 | center(): Vector2 { 54 | return { 55 | x: Math.round((this.v1.x + this.v2.x - 1) / 2), 56 | y: Math.round((this.v1.y + this.v2.y - 1) / 2), 57 | }; 58 | } 59 | 60 | intersects(rect: Rect): boolean { 61 | if (this.v1.x > rect.v2.x || rect.v1.x > this.v2.x) return false; 62 | if (this.v1.y > rect.v2.y || rect.v1.y > this.v2.y) return false; 63 | 64 | return true; 65 | } 66 | 67 | contains(point: Vector2): boolean { 68 | if (this.v1.x > point.x || this.v2.x < point.x) return false; 69 | if (this.v1.y > point.y || this.v2.y < point.y) return false; 70 | 71 | return true; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/struct/table.ts: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "./vector"; 2 | 3 | export class Table { 4 | items: T[] = []; 5 | readonly width: number; 6 | readonly height: number; 7 | 8 | constructor(width: number, height: number) { 9 | this.width = width; 10 | this.height = height; 11 | // ToDo - Initialize empty array 12 | } 13 | 14 | fill(value: T) { 15 | const size = this.width * this.height; 16 | for (let i = 0; i < size; i++) { 17 | this.items[i] = value; 18 | } 19 | } 20 | 21 | get({ x, y }: Vector2): T | undefined { 22 | if (!this.isInBounds({ x, y })) return undefined; 23 | const index = y * this.width + x; 24 | return this.items[index]; 25 | } 26 | 27 | set(pos: Vector2, item: T | undefined): void { 28 | if (this.isInBounds(pos) === false) 29 | throw new Error(`${pos.x + ":" + pos.y} is not in bounds`); 30 | const index = pos.y * this.width + pos.x; 31 | if (item !== undefined) this.items[index] = item; 32 | else delete this.items[index]; 33 | } 34 | 35 | clear(pos: Vector2) { 36 | const index = pos.y * this.width + pos.x; 37 | delete this.items[index]; 38 | } 39 | 40 | isInBounds({ x, y }: Vector2) { 41 | if (x < 0 || x >= this.width || y < 0 || y >= this.height) { 42 | return false; 43 | } else { 44 | return true; 45 | } 46 | } 47 | 48 | getNeighbors( 49 | pos: Vector2, 50 | predicate?: (pos: Vector2, t: T | undefined) => Boolean, 51 | topology: "four" | "eight" = "eight" 52 | ): Vector2[] { 53 | let neighbors: Vector2[] = []; 54 | 55 | neighbors.push({ x: pos.x + 1, y: pos.y }); 56 | neighbors.push({ x: pos.x, y: pos.y + -1 }); 57 | neighbors.push({ x: pos.x + -1, y: pos.y }); 58 | neighbors.push({ x: pos.x, y: pos.y + 1 }); 59 | 60 | if (topology === "eight") { 61 | neighbors.push({ x: pos.x + 1, y: pos.y - 1 }); 62 | neighbors.push({ x: pos.x + -1, y: pos.y + -1 }); 63 | neighbors.push({ x: pos.x + -1, y: pos.y + 1 }); 64 | neighbors.push({ x: pos.x + 1, y: pos.y + 1 }); 65 | } 66 | 67 | neighbors = neighbors.filter((v) => this.isInBounds(v)); 68 | 69 | if (predicate) { 70 | neighbors = neighbors.filter((v) => predicate(v, this.get(v))); 71 | } 72 | 73 | return neighbors; 74 | } 75 | 76 | floodFillSelect(pos: Vector2, targetValue?: T | undefined): Vector2[] { 77 | if (!targetValue) { 78 | targetValue = this.get(pos); 79 | } 80 | 81 | // If given a target value, must match start position 82 | if (this.get(pos) !== targetValue) { 83 | return []; 84 | } 85 | 86 | const horizon = [pos]; 87 | // "x:y", for quick indexing 88 | const floodFill = new Set(); 89 | 90 | while (horizon.length) { 91 | const point = horizon.shift()!; 92 | const value = this.get(point); 93 | 94 | // If not the right type of value, we're done. 95 | if (value !== targetValue) continue; 96 | // If we've already marked this spot 97 | if (floodFill.has(`${point.x}:${point.y}`)) continue; 98 | 99 | // If it is the proper value, collect it 100 | floodFill.add(`${point.x}:${point.y}`); 101 | 102 | // The add it's neighbors to the search horizon 103 | const neighbors = this.getNeighbors(point, undefined, "four"); 104 | horizon.push(...neighbors); 105 | } 106 | 107 | const fill = Array.from(floodFill.keys()).map((str) => { 108 | const [x, y] = str.split(":"); 109 | return { 110 | x: Number.parseInt(x), 111 | y: Number.parseInt(y), 112 | }; 113 | }); 114 | 115 | return fill; 116 | } 117 | 118 | filter(match: (v: Vector2, val: T | undefined) => boolean): Vector2[] { 119 | const matches: Vector2[] = []; 120 | 121 | for (let y = 0; y < this.height; y++) { 122 | for (let x = 0; x < this.width; x++) { 123 | if (match({ x, y }, this.get({ x, y }))) { 124 | matches.push({ x, y }); 125 | } 126 | } 127 | } 128 | 129 | return matches; 130 | } 131 | 132 | clone(): Table { 133 | const t = new Table(this.width, this.height); 134 | t.items = this.items.slice(); 135 | return t; 136 | } 137 | 138 | isSameSize(other: Table): boolean { 139 | return this.width === other.width && this.height === other.height; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/struct/vector.ts: -------------------------------------------------------------------------------- 1 | export interface Vector2 { 2 | x: number; 3 | y: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/terminal/__snapshots__/unicodemap.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`unicodemap Should have unicode values 1`] = ` 4 | Object { 5 | "161": 173, 6 | "162": 155, 7 | "163": 156, 8 | "165": 157, 9 | "167": 21, 10 | "170": 166, 11 | "171": 174, 12 | "172": 170, 13 | "176": 248, 14 | "177": 241, 15 | "178": 253, 16 | "181": 230, 17 | "182": 20, 18 | "183": 250, 19 | "186": 167, 20 | "187": 175, 21 | "188": 172, 22 | "189": 171, 23 | "191": 168, 24 | "196": 142, 25 | "197": 143, 26 | "198": 146, 27 | "199": 128, 28 | "201": 144, 29 | "209": 165, 30 | "214": 153, 31 | "220": 154, 32 | "223": 225, 33 | "224": 133, 34 | "225": 160, 35 | "226": 131, 36 | "228": 132, 37 | "229": 134, 38 | "230": 145, 39 | "231": 135, 40 | "232": 138, 41 | "233": 130, 42 | "234": 136, 43 | "235": 137, 44 | "236": 141, 45 | "237": 161, 46 | "238": 140, 47 | "239": 139, 48 | "241": 164, 49 | "242": 149, 50 | "243": 162, 51 | "244": 147, 52 | "246": 148, 53 | "247": 246, 54 | "249": 151, 55 | "250": 163, 56 | "251": 150, 57 | "252": 129, 58 | "255": 152, 59 | "402": 159, 60 | "8226": 7, 61 | "8252": 19, 62 | "8319": 252, 63 | "8359": 158, 64 | "8592": 27, 65 | "8593": 24, 66 | "8594": 26, 67 | "8595": 25, 68 | "8596": 29, 69 | "8597": 18, 70 | "8616": 23, 71 | "8729": 249, 72 | "8730": 251, 73 | "8734": 236, 74 | "8735": 28, 75 | "8745": 239, 76 | "8776": 247, 77 | "8801": 240, 78 | "8804": 243, 79 | "8805": 242, 80 | "8962": 127, 81 | "8976": 169, 82 | "8992": 244, 83 | "8993": 245, 84 | "915": 226, 85 | "920": 233, 86 | "931": 228, 87 | "934": 232, 88 | "937": 234, 89 | "945": 224, 90 | "9472": 196, 91 | "9474": 179, 92 | "948": 235, 93 | "9484": 218, 94 | "9488": 191, 95 | "949": 238, 96 | "9492": 192, 97 | "9496": 217, 98 | "9500": 195, 99 | "9508": 180, 100 | "9516": 194, 101 | "9524": 193, 102 | "9532": 197, 103 | "9552": 205, 104 | "9553": 186, 105 | "9554": 213, 106 | "9555": 214, 107 | "9556": 201, 108 | "9557": 184, 109 | "9558": 183, 110 | "9559": 187, 111 | "9560": 212, 112 | "9561": 211, 113 | "9562": 200, 114 | "9563": 190, 115 | "9564": 189, 116 | "9565": 188, 117 | "9566": 198, 118 | "9567": 199, 119 | "9568": 204, 120 | "9569": 181, 121 | "9570": 182, 122 | "9571": 185, 123 | "9572": 209, 124 | "9573": 210, 125 | "9574": 203, 126 | "9575": 207, 127 | "9576": 208, 128 | "9577": 202, 129 | "9578": 216, 130 | "9579": 215, 131 | "9580": 206, 132 | "960": 227, 133 | "9600": 223, 134 | "9604": 220, 135 | "9608": 219, 136 | "9612": 221, 137 | "9616": 222, 138 | "9617": 176, 139 | "9618": 177, 140 | "9619": 178, 141 | "963": 229, 142 | "9632": 254, 143 | "964": 231, 144 | "9644": 22, 145 | "9650": 30, 146 | "9658": 16, 147 | "966": 237, 148 | "9660": 31, 149 | "9668": 17, 150 | "9675": 9, 151 | "9688": 8, 152 | "9689": 10, 153 | "9786": 1, 154 | "9787": 2, 155 | "9788": 15, 156 | "9792": 12, 157 | "9794": 11, 158 | "9824": 6, 159 | "9827": 5, 160 | "9829": 3, 161 | "9830": 4, 162 | "9834": 13, 163 | "9835": 14, 164 | } 165 | `; 166 | -------------------------------------------------------------------------------- /src/terminal/char-code.spec.ts: -------------------------------------------------------------------------------- 1 | import { CharCode } from "./char-code"; 2 | 3 | describe("CharCode", () => { 4 | it("Should have all CharCodes listed", () => { 5 | expect(CharCode).toMatchSnapshot(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/terminal/color.spec.ts: -------------------------------------------------------------------------------- 1 | import { Color } from "./color"; 2 | 3 | describe("Color", () => { 4 | it("Can create basic colors from rgb", () => { 5 | const c = new Color(10, 20, 30); 6 | expect(c.r).toEqual(10); 7 | expect(c.g).toEqual(20); 8 | expect(c.b).toEqual(30); 9 | }); 10 | 11 | it("Can compare colors", () => { 12 | const tests: [Color, Color, boolean][] = [ 13 | [new Color(5, 10, 20), new Color(5, 10, 20), true], 14 | [new Color(0, 0, 0), new Color(0, 0, 0), true], 15 | [new Color(255, 255, 0), new Color(255, 255, 0), true], 16 | [new Color(255, 0, 255), new Color(255, 255, 0), false], 17 | [new Color(1, 2, 3), new Color(4, 5, 6), false], 18 | ]; 19 | 20 | for (let [c1, c2, bool] of tests) { 21 | expect(c1.isEqual(c2)).toEqual(bool); 22 | expect(c2.isEqual(c1)).toEqual(bool); 23 | 24 | expect(c1.isEqual(c1)).toEqual(true); 25 | expect(c2.isEqual(c2)).toEqual(true); 26 | } 27 | }); 28 | 29 | it("Can get a css color", () => { 30 | expect(new Color(10, 20, 30).cssColor()).toEqual("rgb(10,20,30)"); 31 | expect(new Color(100, 100, 255).cssColor()).toEqual("rgb(100,100,255)"); 32 | }); 33 | 34 | it("Can add another color", () => { 35 | const c = new Color(10, 20, 30); 36 | const n = c.add(new Color(10, 10, 10)); 37 | 38 | expect(n.r).toEqual(20); 39 | expect(n.g).toEqual(30); 40 | expect(n.b).toEqual(40); 41 | 42 | const n2 = c.add(new Color(10, 10, 10), 0.5); 43 | expect(n2.r).toEqual(15); 44 | expect(n2.g).toEqual(25); 45 | expect(n2.b).toEqual(35); 46 | }); 47 | 48 | it("Can blend another color", () => { 49 | const c = new Color(10, 20, 30); 50 | const n = c.blend(new Color(10, 10, 10)); 51 | 52 | expect(n.r).toEqual(10); 53 | expect(n.g).toEqual(15); 54 | expect(n.b).toEqual(20); 55 | }); 56 | 57 | it("Can blend a percent", () => { 58 | const c = new Color(10, 20, 30); 59 | const n = c.blendPercent(new Color(10, 10, 10), 50); 60 | 61 | expect(n.r).toEqual(10); 62 | expect(n.g).toEqual(15); 63 | expect(n.b).toEqual(20); 64 | }); 65 | 66 | it("Can get grayscale", () => { 67 | const c = new Color(10, 20, 30); 68 | const g = c.toGrayscale(); 69 | 70 | expect(g.r).toEqual(20); 71 | expect(g.g).toEqual(20); 72 | expect(g.b).toEqual(20); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/terminal/display.spec.ts: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "../struct/vector"; 2 | import { Display } from "./display"; 3 | import { Glyph } from "./glyph"; 4 | 5 | describe("Display", () => { 6 | it("Can get the Size", () => { 7 | const d = new Display(10, 20); 8 | 9 | expect(d.width).toEqual(10); 10 | expect(d.height).toEqual(20); 11 | 12 | expect(d.size()).toEqual({ x: 10, y: 20 }); 13 | }); 14 | 15 | it("Won't update the table until rendering", () => { 16 | const d = new Display(10, 10); 17 | 18 | d.setGlyph({ x: 5, y: 5 }, new Glyph("x")); 19 | d.render(() => {}); 20 | d.setGlyph({ x: 5, y: 5 }, new Glyph("x")); 21 | 22 | d.render((pos, glyph) => { 23 | if (pos.x === 5 && pos.y === 5) { 24 | expect(glyph.isEqual(new Glyph("x"))).toBeTruthy(); 25 | } 26 | }); 27 | }); 28 | 29 | it("Won't store the glyph if out of bounds", () => { 30 | const d = new Display(10, 10); 31 | 32 | d.setGlyph({ x: -1, y: -1 }, new Glyph("x")); 33 | 34 | d.render((pos, g) => { 35 | throw new Error("Render shouldn't call anything. Unexpected Glyph"); 36 | }); 37 | }); 38 | 39 | it("Can render", () => { 40 | const d = new Display(3, 3); 41 | d.setGlyph({ x: 0, y: 0 }, new Glyph("1")); 42 | d.setGlyph({ x: 1, y: 0 }, new Glyph("2")); 43 | d.setGlyph({ x: 2, y: 0 }, new Glyph("3")); 44 | d.setGlyph({ x: 0, y: 1 }, new Glyph("4")); 45 | d.setGlyph({ x: 1, y: 1 }, new Glyph("5")); 46 | d.setGlyph({ x: 2, y: 1 }, new Glyph("6")); 47 | d.setGlyph({ x: 0, y: 2 }, new Glyph("7")); 48 | d.setGlyph({ x: 1, y: 2 }, new Glyph("8")); 49 | d.setGlyph({ x: 2, y: 2 }, new Glyph("9")); 50 | 51 | const vectors: Vector2[] = []; 52 | const glyphs: Glyph[] = []; 53 | 54 | d.render((pos, glyph) => { 55 | vectors.push(pos); 56 | glyphs.push(glyph); 57 | }); 58 | 59 | expect(vectors).toHaveLength(9); 60 | expect(vectors).toEqual([ 61 | { x: 2, y: 2 }, 62 | { x: 1, y: 2 }, 63 | { x: 0, y: 2 }, 64 | { x: 2, y: 1 }, 65 | { x: 1, y: 1 }, 66 | { x: 0, y: 1 }, 67 | { x: 2, y: 0 }, 68 | { x: 1, y: 0 }, 69 | { x: 0, y: 0 }, 70 | ]); 71 | 72 | expect(glyphs).toHaveLength(9); 73 | for (let i = 0; i < 9; i++) { 74 | expect(glyphs[i].isEqual(new Glyph((i + 1).toString()))); 75 | } 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /src/terminal/display.ts: -------------------------------------------------------------------------------- 1 | import { Glyph } from "./glyph"; 2 | import { Vector2 } from "../struct/vector"; 3 | import { Table } from "../struct/table"; 4 | 5 | type RenderGlyph = (pos: Vector2, glyph: Glyph) => any; 6 | 7 | /** Represents glyph data, agnostic to how it is rendered. */ 8 | export class Display { 9 | private glyphs: Table; 10 | private changedGlyphs: Table; 11 | 12 | readonly width: number; 13 | readonly height: number; 14 | 15 | /** 16 | * Creates a new Display 17 | * @param width - The number of glyphs wide the display is 18 | * @param height - The number of glyphs tall the display is 19 | */ 20 | constructor(width: number, height: number) { 21 | this.width = width; 22 | this.height = height; 23 | this.glyphs = new Table(width, height); 24 | this.changedGlyphs = new Table(width, height); 25 | } 26 | 27 | /** 28 | * Returns a Vector representing the width/height of the display. 29 | */ 30 | size(): Vector2 { 31 | return { x: this.width, y: this.height }; 32 | } 33 | 34 | /** 35 | * Sets a single glyph in the display. 36 | * 37 | * @param pos Vector2 - The position of the Glyph 38 | * @param glyph Glyph - The Glyph 39 | */ 40 | setGlyph(pos: Vector2, glyph: Glyph) { 41 | if (this.glyphs.isInBounds(pos) === false) return; 42 | if (glyph.isEqual(this.glyphs.get(pos))) { 43 | this.changedGlyphs.set(pos, undefined); 44 | } else { 45 | this.changedGlyphs.set(pos, glyph); 46 | } 47 | } 48 | 49 | /** 50 | * Calls the callback with each x/y/glyph pair. 51 | * The terminal needs to decide how to render the display. 52 | * The callback is called bottom to top, right to left. 53 | */ 54 | render(callback: RenderGlyph) { 55 | for (let y = this.height - 1; y >= 0; y--) { 56 | for (let x = this.width - 1; x >= 0; x--) { 57 | const glyph = this.changedGlyphs.get({ x, y }); 58 | if (!glyph) continue; 59 | callback({ x, y }, glyph); 60 | 61 | this.glyphs.set({ x, y }, glyph); 62 | this.changedGlyphs.set({ x, y }, undefined); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/terminal/glyph.spec.ts: -------------------------------------------------------------------------------- 1 | import { Glyph } from "./glyph"; 2 | import { Color } from "./color"; 3 | 4 | describe("Glyph", () => { 5 | it("Can create a new Glyph", () => { 6 | const g = new Glyph("f", Color.Green, Color.Black); 7 | 8 | expect(g.char).toEqual("f".charCodeAt(0)); 9 | expect(g.fore.isEqual(Color.Green)).toBeTruthy(); 10 | expect(g.back.isEqual(Color.Black)).toBeTruthy(); 11 | 12 | const defaults = new Glyph("f"); 13 | expect(defaults.fore.isEqual(Color.White)).toBeTruthy(); 14 | expect(defaults.back.isEqual(Color.Black)).toBeTruthy(); 15 | }); 16 | 17 | it("Can create a new Glyph from charcode", () => { 18 | const g = Glyph.fromCharCode(102, Color.Green, Color.Black); 19 | expect(g.char).toEqual(102); 20 | expect(g.fore.isEqual(Color.Green)).toBeTruthy(); 21 | expect(g.back.isEqual(Color.Black)).toBeTruthy(); 22 | 23 | const defaults = Glyph.fromCharCode(102); 24 | expect(defaults.fore.isEqual(Color.White)).toBeTruthy(); 25 | expect(defaults.back.isEqual(Color.Black)).toBeTruthy(); 26 | }); 27 | 28 | it("Can confirm equal", () => { 29 | const g = new Glyph("g", Color.Blue, Color.Green); 30 | const f = new Glyph("g", Color.Blue, Color.Green); 31 | 32 | const x = new Glyph("g", Color.Blue, Color.Blue); 33 | const y = new Glyph("g", Color.Green, Color.Green); 34 | const z = new Glyph("z", Color.Blue, Color.Green); 35 | 36 | const tests: [Glyph, Glyph, boolean][] = [ 37 | [g, f, true], 38 | [g, x, false], 39 | [g, y, false], 40 | [g, z, false], 41 | ]; 42 | 43 | for (let [g1, g2, bool] of tests) { 44 | expect(g1.isEqual(g2)).toEqual(bool); 45 | expect(g2.isEqual(g1)).toEqual(bool); 46 | } 47 | 48 | // Test for non-glyph 49 | expect(g.isEqual({})).toBeFalsy(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/terminal/glyph.ts: -------------------------------------------------------------------------------- 1 | import { Color } from "./color"; 2 | 3 | /** 4 | * Represents a glyph to be drawn to the screen. 5 | */ 6 | export class Glyph { 7 | readonly char: number; 8 | readonly fore: Color; 9 | readonly back: Color; 10 | 11 | /** 12 | * Creates a glyph from a charCode. 13 | * 14 | * @param char - A number representing a charCode. 15 | * @param fore - A foreground color (default white) 16 | * @param back - A background color (default black) 17 | */ 18 | static fromCharCode( 19 | char: number, 20 | fore: Color = Color.White, 21 | back: Color = Color.Black 22 | ) { 23 | return new Glyph(String.fromCharCode(char), fore, back); 24 | } 25 | 26 | /** 27 | * Creates a Glyph from a single character. 28 | * 29 | * @param char - A single character. 30 | * @param fore - A foreground color (default white) 31 | * @param back - A background color (default black) 32 | */ 33 | constructor( 34 | char: string, 35 | fore: Color = Color.White, 36 | back: Color = Color.Black 37 | ) { 38 | this.char = char.charCodeAt(0); 39 | this.fore = fore; 40 | this.back = back; 41 | } 42 | 43 | /** 44 | * Checks to see if two glyphs are equal. 45 | * @param other Glyph - The other glyph. 46 | */ 47 | isEqual(other: any) { 48 | if (other instanceof Glyph === false) return false; 49 | return ( 50 | this.char === other.char && 51 | this.fore.isEqual(other.fore) && 52 | this.back.isEqual(other.back) 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/terminal/index.ts: -------------------------------------------------------------------------------- 1 | export { CanvasTerminal, Font } from "./canvas-terminal"; 2 | export { RetroTerminal } from "./retro-terminal"; 3 | export { Color } from "./color"; 4 | export { Glyph } from "./glyph"; 5 | export { CharCode } from "./char-code"; 6 | export { BaseTerminal } from "./terminal"; 7 | -------------------------------------------------------------------------------- /src/terminal/memory-terminal.spec.ts: -------------------------------------------------------------------------------- 1 | import { Color, Glyph } from "."; 2 | import { MemoryTerminal } from "./memory-terminal"; 3 | 4 | describe("MemoryTerminal", () => { 5 | it("will write glyphs to memory", () => { 6 | const t = new MemoryTerminal({ width: 10, height: 10 }); 7 | const g = new Glyph(" ", Color.Green, Color.Blue); 8 | t.drawGlyph({ x: 0, y: 0 }, g); 9 | 10 | expect(t.glyphs.get({ x: 0, y: 0 })).toEqual(g); 11 | expect(t.glyphs.get({ x: 1, y: 0 })).toEqual(undefined); 12 | 13 | t.delete(); 14 | }); 15 | 16 | it("will not throw when writing glyphs out of bounds", () => { 17 | const t = new MemoryTerminal({ width: 10, height: 10 }); 18 | const g = new Glyph(" ", Color.Green, Color.Blue); 19 | t.drawGlyph({ x: -1, y: 0 }, g); 20 | }); 21 | 22 | it("will return the same value for windowToTilePoint", () => { 23 | const t = new MemoryTerminal({ width: 10, height: 10 }); 24 | 25 | expect(t.windowToTilePoint({ x: 5, y: 10 })).toEqual({ x: 5, y: 10 }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/terminal/memory-terminal.ts: -------------------------------------------------------------------------------- 1 | import { BaseTerminal } from "."; 2 | import { Table, Vector2 } from "../struct"; 3 | import { Glyph } from "./glyph"; 4 | import { TerminalConfig } from "./terminal"; 5 | 6 | export class MemoryTerminal extends BaseTerminal { 7 | windowToTilePoint(pixel: Vector2): Vector2 { 8 | return pixel; 9 | } 10 | delete(): void {} 11 | 12 | glyphs: Table; 13 | 14 | constructor(config: TerminalConfig) { 15 | super(config); 16 | this.glyphs = new Table(config.width, config.height); 17 | } 18 | 19 | drawGlyph(pos: Vector2, glyph: Glyph): void { 20 | if (this.glyphs.isInBounds(pos) === false) return; 21 | this.glyphs.set(pos, glyph); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/terminal/terminal.spec.ts: -------------------------------------------------------------------------------- 1 | import { Table, Vector2 } from "../struct"; 2 | import { Color } from "./color"; 3 | import { Glyph } from "./glyph"; 4 | import { BaseTerminal, TerminalConfig } from "./terminal"; 5 | 6 | class TestTerminal extends BaseTerminal { 7 | windowToTilePoint(pixel: Vector2): Vector2 { 8 | throw new Error("Method not implemented."); 9 | } 10 | delete(): void { 11 | throw new Error("Method not implemented."); 12 | } 13 | table: Table; 14 | constructor(config: TerminalConfig) { 15 | super(config); 16 | 17 | this.table = new Table(config.width, config.height); 18 | } 19 | drawGlyph(pos: Vector2, glyph: Glyph): void { 20 | this.table.set(pos, glyph); 21 | } 22 | } 23 | 24 | describe("BaseTerminal", () => { 25 | it("Can create a new base terminal", () => { 26 | const t = new TestTerminal({ 27 | width: 10, 28 | height: 10, 29 | foreColor: Color.Yellow, 30 | backColor: Color.Green, 31 | }); 32 | 33 | expect(t.width).toEqual(10); 34 | expect(t.height).toEqual(10); 35 | expect(t.foreColor.isEqual(Color.Yellow)).toBeTruthy(); 36 | expect(t.backColor.isEqual(Color.Green)).toBeTruthy(); 37 | }); 38 | 39 | it("Can draw a charCode", () => { 40 | const t = new TestTerminal({ 41 | width: 10, 42 | height: 10, 43 | }); 44 | 45 | t.drawCharCode({ x: 1, y: 2 }, 102); 46 | let glyph = t.table.get({ x: 1, y: 2 }); 47 | expect(glyph?.char).toEqual(102); 48 | expect(glyph?.fore.isEqual(Color.White)).toBeTruthy(); 49 | expect(glyph?.back.isEqual(Color.Black)).toBeTruthy(); 50 | 51 | t.drawCharCode({ x: 1, y: 2 }, 102, Color.Green, Color.Yellow); 52 | glyph = t.table.get({ x: 1, y: 2 }); 53 | expect(glyph?.char).toEqual(102); 54 | expect(glyph?.fore.isEqual(Color.Green)).toBeTruthy(); 55 | expect(glyph?.back.isEqual(Color.Yellow)).toBeTruthy(); 56 | }); 57 | 58 | it("Can get the size", () => { 59 | const t = new TestTerminal({ width: 10, height: 20 }); 60 | 61 | expect(t.size()).toEqual({ x: 10, y: 20 }); 62 | }); 63 | 64 | it("Can clear", () => { 65 | const t = new TestTerminal({ width: 10, height: 10 }); 66 | t.clear(); 67 | 68 | for (let x = 0; x < 10; x++) { 69 | for (let y = 0; y < 10; y++) { 70 | expect(t.table.get({ x, y })?.isEqual(new Glyph(" "))); 71 | } 72 | } 73 | }); 74 | 75 | it("Can writeAt", () => { 76 | const t = new TestTerminal({ 77 | width: 10, 78 | height: 10, 79 | }); 80 | 81 | t.writeAt({ x: 1, y: 1 }, "abc"); 82 | 83 | const a = t.table.get({ x: 1, y: 1 })!; 84 | const b = t.table.get({ x: 2, y: 1 })!; 85 | const c = t.table.get({ x: 3, y: 1 })!; 86 | 87 | expect(a.char).toEqual("a".charCodeAt(0)); 88 | expect(b.char).toEqual("b".charCodeAt(0)); 89 | expect(c.char).toEqual("c".charCodeAt(0)); 90 | 91 | t.writeAt({ x: 1, y: 1 }, "abc", Color.Green, Color.Blue); 92 | 93 | const a2 = t.table.get({ x: 1, y: 1 })!; 94 | const b2 = t.table.get({ x: 2, y: 1 })!; 95 | const c2 = t.table.get({ x: 3, y: 1 })!; 96 | 97 | expect(a2.isEqual(new Glyph("a", Color.Green, Color.Blue))); 98 | expect(b2.isEqual(new Glyph("b", Color.Green, Color.Blue))); 99 | expect(c2.isEqual(new Glyph("c", Color.Green, Color.Blue))); 100 | }); 101 | 102 | it("Will truncate writeAt if too long", () => { 103 | const t = new TestTerminal({ width: 5, height: 5 }); 104 | t.writeAt({ x: 0, y: 0 }, "Hello!"); 105 | expect(t.table.get({ x: 5, y: 0 })).toEqual(undefined); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /src/terminal/terminal.ts: -------------------------------------------------------------------------------- 1 | import { Vector2 } from "../struct/vector"; 2 | import { Color } from "./color"; 3 | import { Glyph } from "./glyph"; 4 | 5 | export interface TerminalConfig { 6 | width: number; 7 | height: number; 8 | foreColor?: Color; 9 | backColor?: Color; 10 | } 11 | 12 | export abstract class BaseTerminal { 13 | readonly width: number; 14 | readonly height: number; 15 | 16 | foreColor: Color; 17 | backColor: Color; 18 | 19 | constructor(config: TerminalConfig) { 20 | this.width = config.width; 21 | this.height = config.height; 22 | this.foreColor = config.foreColor ?? Color.White; 23 | this.backColor = config.backColor ?? Color.Black; 24 | } 25 | 26 | size(): Vector2 { 27 | return { 28 | x: this.width, 29 | y: this.height, 30 | }; 31 | } 32 | 33 | clear() { 34 | this.fill( 35 | { x: 0, y: 0 }, 36 | { x: this.width - 1, y: this.height - 1 }, 37 | new Glyph(" ") 38 | ); 39 | } 40 | 41 | fill(v1: Vector2, v2: Vector2, glyph: Glyph) { 42 | for (let x = v1.x; x <= v2.x; x++) { 43 | for (let y = v1.y; y <= v2.y; y++) { 44 | this.drawGlyph({ x, y }, glyph); 45 | } 46 | } 47 | } 48 | 49 | writeAt( 50 | pos: Vector2, 51 | text: string, 52 | fore = this.foreColor, 53 | back = this.backColor 54 | ) { 55 | for (let i = 0; i < text.length; i++) { 56 | if (pos.x + i >= this.width) break; 57 | this.drawGlyph( 58 | { 59 | x: pos.x + i, 60 | y: pos.y, 61 | }, 62 | Glyph.fromCharCode(text.charCodeAt(i), fore, back) 63 | ); 64 | } 65 | } 66 | 67 | drawCharCode( 68 | pos: Vector2, 69 | charCode: number, 70 | foreColor = this.foreColor, 71 | backColor = this.backColor 72 | ) { 73 | this.drawGlyph(pos, Glyph.fromCharCode(charCode, foreColor, backColor)); 74 | } 75 | 76 | abstract drawGlyph(pos: Vector2, glyph: Glyph): void; 77 | abstract windowToTilePoint(pixel: Vector2): Vector2; 78 | abstract delete(): void; 79 | } 80 | -------------------------------------------------------------------------------- /src/terminal/unicodemap.spec.ts: -------------------------------------------------------------------------------- 1 | import { unicodeMap } from "./unicodemap"; 2 | 3 | describe("unicodemap", () => { 4 | it("Should have unicode values", () => { 5 | expect(unicodeMap).toMatchSnapshot(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "module": "es2015", 6 | "lib": ["es2015", "es2016", "es2017", "dom"], 7 | "strict": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "allowSyntheticDefaultImports": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "declarationDir": "dist/types", 14 | "outDir": "dist/lib", 15 | "typeRoots": ["node_modules/@types"] 16 | }, 17 | "include": ["src"] 18 | } 19 | --------------------------------------------------------------------------------