├── src ├── app.ts ├── glyph.ts ├── point.ts ├── actor.ts ├── text-utility.ts ├── tile.ts ├── game-state.ts ├── message-log.ts ├── status-line.ts ├── input-utility.ts ├── pedro.ts ├── player.ts ├── tiny-pedro.ts ├── map.ts └── game.ts ├── tsconfig.json ├── webpack.config.js ├── index.html ├── package.json ├── .gitignore └── README.md /src/app.ts: -------------------------------------------------------------------------------- 1 | import { Game } from "./game"; 2 | 3 | document.body.onload = () => { 4 | var game = new Game(); 5 | } -------------------------------------------------------------------------------- /src/glyph.ts: -------------------------------------------------------------------------------- 1 | export class Glyph { 2 | constructor(public character: string, public foregroundColor?: string, public backgroundColor?: string) { } 3 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "es2015" 7 | ] 8 | }, 9 | "include": [ 10 | "src/*" 11 | ] 12 | } -------------------------------------------------------------------------------- /src/point.ts: -------------------------------------------------------------------------------- 1 | export class Point { 2 | constructor(public x: number, public y: number) { } 3 | 4 | equals(point: Point): boolean { 5 | return this.x == point.x && this.y == point.y; 6 | } 7 | 8 | toKey(): string { 9 | return this.x + "," + this.y; 10 | } 11 | } -------------------------------------------------------------------------------- /src/actor.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "./point"; 2 | import { Glyph } from "./glyph"; 3 | 4 | export const enum ActorType { 5 | Player, 6 | Pedro, 7 | TinyPedro 8 | } 9 | 10 | export interface Actor { 11 | position: Point; 12 | glyph: Glyph; 13 | type: ActorType; 14 | 15 | act(): Promise; 16 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/app.ts', 5 | module: { 6 | rules:[{ 7 | test: /\.tsx?$/, 8 | use: 'ts-loader', 9 | exclude: /node_modules/ 10 | }] 11 | }, 12 | resolve: { 13 | extensions: ['.ts', '.tsx', '.js'] 14 | }, 15 | output: { 16 | filename: 'app.js', 17 | path: path.resolve(__dirname, 'dist') 18 | }, 19 | mode: 'development' 20 | }; -------------------------------------------------------------------------------- /src/text-utility.ts: -------------------------------------------------------------------------------- 1 | export function padLeft(text: string, length: number, character?: string): string { 2 | let char = character || " "; 3 | while (text.length < length) { 4 | text = char + text; 5 | } 6 | return text; 7 | } 8 | 9 | export function padRight(text: string, length: number, character?: string): string { 10 | let char = character || " "; 11 | while (text.length < length) { 12 | text += char; 13 | } 14 | return text; 15 | } -------------------------------------------------------------------------------- /src/tile.ts: -------------------------------------------------------------------------------- 1 | import { Glyph } from "./glyph"; 2 | 3 | export const enum TileType { 4 | Floor, 5 | Box, 6 | SearchedBox, 7 | DestroyedBox 8 | } 9 | 10 | export class Tile { 11 | static readonly floor = new Tile(TileType.Floor, new Glyph(".")); 12 | static readonly box = new Tile(TileType.Box, new Glyph("#", "#654321")); 13 | static readonly searchedBox = new Tile(TileType.SearchedBox, new Glyph("#", "#666")); 14 | static readonly destroyedBox = new Tile(TileType.DestroyedBox, new Glyph("x", "#555")); 15 | 16 | constructor(public readonly type: TileType, public readonly glyph: Glyph) { } 17 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic rot.js example with TypeScript 5 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/game-state.ts: -------------------------------------------------------------------------------- 1 | export class GameState { 2 | foundPineapple: boolean; 3 | pineappleWasDestroyed: boolean; 4 | playerWasCaught: boolean; 5 | 6 | constructor() { 7 | this.reset(); 8 | } 9 | 10 | reset(): void { 11 | this.foundPineapple = false; 12 | this.pineappleWasDestroyed = false; 13 | this.playerWasCaught = false; 14 | } 15 | 16 | doStartNextRound(): boolean { 17 | return this.foundPineapple; 18 | } 19 | 20 | doRestartGame(): boolean { 21 | return this.pineappleWasDestroyed || this.playerWasCaught; 22 | } 23 | 24 | isGameOver(): boolean { 25 | return this.foundPineapple || this.pineappleWasDestroyed || this.playerWasCaught; 26 | } 27 | } -------------------------------------------------------------------------------- /src/message-log.ts: -------------------------------------------------------------------------------- 1 | import { Game } from "./game"; 2 | import { Point } from "./point"; 3 | 4 | export class MessageLog { 5 | private lines: string[]; 6 | 7 | constructor(private game: Game, private position: Point, private maxWidth: number, private maxLines: number) { 8 | this.lines = []; 9 | } 10 | 11 | clear(): void { 12 | this.lines = []; 13 | } 14 | 15 | appendText(text: string): void { 16 | this.lines.splice(0, 0, text); 17 | if (this.lines.length > this.maxLines) { 18 | this.lines.splice(this.maxLines, this.lines.length - this.maxLines); 19 | } 20 | } 21 | 22 | draw(): void { 23 | let linePosition = new Point(this.position.x, this.position.y); 24 | for (let index = 0; index < this.maxLines && index < this.lines.length; ++index) { 25 | this.game.drawText(linePosition, this.lines[index], this.maxWidth); 26 | ++linePosition.y; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rotjs-typescript-basics", 3 | "version": "1.0.0", 4 | "description": "Using the rot.js library with TypeScript in a basic example.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "watch": "webpack --watch", 9 | "serve": "http-server --port=8085 -c-1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/Mizar999/rotjs-typescript-basics.git" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/Mizar999/rotjs-typescript-basics/issues" 20 | }, 21 | "homepage": "https://github.com/Mizar999/rotjs-typescript-basics#readme", 22 | "devDependencies": { 23 | "concurrently": "^7.6.0", 24 | "http-server": "^14.1.1", 25 | "rot-js": "^2.2.0", 26 | "ts-loader": "^9.4.2", 27 | "typescript": "^4.9.4", 28 | "webpack": "^5.75.0", 29 | "webpack-cli": "^5.0.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/status-line.ts: -------------------------------------------------------------------------------- 1 | import { Game } from "./game"; 2 | import { Point } from "./point"; 3 | import { padRight, padLeft } from "./text-utility"; 4 | 5 | export class StatusLine { 6 | turns: number; 7 | pineapples: number; 8 | boxes: number; 9 | maxBoxes: number; 10 | 11 | constructor(private game: Game, private position: Point, private maxWidth: number, params?: any) { 12 | if (!params) { 13 | params = {}; 14 | } 15 | this.turns = params.turns || 0; 16 | this.pineapples = params.ananas || 0; 17 | this.boxes = params.boxes || 0; 18 | this.maxBoxes = params.maxBoxes || 0; 19 | } 20 | 21 | reset(): void { 22 | this.turns = 0; 23 | this.pineapples = 0; 24 | this.boxes = 0; 25 | this.maxBoxes = 0; 26 | } 27 | 28 | draw(): void { 29 | let text = `turns: ${padRight(this.turns.toString(), 6)} pineapples: ${padRight(this.pineapples.toString(), 6)} boxes: ${padLeft(this.boxes.toString(), 2)} / ${padLeft(this.maxBoxes.toString(), 2)}`; 30 | this.game.drawText(this.position, text, this.maxWidth); 31 | } 32 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /src/input-utility.ts: -------------------------------------------------------------------------------- 1 | export class InputUtility { 2 | private static processInputCallback: (event: KeyboardEvent) => any; 3 | private static resolve: (value?: any) => void; 4 | 5 | static waitForInput(handleInput: (event: KeyboardEvent) => boolean): Promise { 6 | return new Promise(resolve => { 7 | if (InputUtility.processInputCallback !== undefined) { 8 | InputUtility.stopProcessing(); 9 | } 10 | 11 | InputUtility.resolve = resolve; 12 | InputUtility.processInputCallback = (event: KeyboardEvent) => InputUtility.processInput(event, handleInput); 13 | window.addEventListener("keydown", InputUtility.processInputCallback); 14 | }); 15 | } 16 | 17 | private static processInput(event: KeyboardEvent, handleInput: (event: KeyboardEvent) => boolean): void { 18 | if (handleInput(event)) { 19 | InputUtility.stopProcessing(); 20 | } 21 | } 22 | 23 | private static stopProcessing(): void { 24 | window.removeEventListener("keydown", InputUtility.processInputCallback); 25 | InputUtility.processInputCallback = undefined; 26 | InputUtility.resolve(); 27 | } 28 | } -------------------------------------------------------------------------------- /src/pedro.ts: -------------------------------------------------------------------------------- 1 | import { Path } from "rot-js"; 2 | import { Game } from "./game"; 3 | import { Actor, ActorType } from "./actor"; 4 | import { Point } from "./point"; 5 | import { Glyph } from "./glyph"; 6 | 7 | export class Pedro implements Actor { 8 | glyph: Glyph; 9 | type: ActorType; 10 | private path: Point[]; 11 | 12 | constructor(private game: Game, public position: Point) { 13 | this.glyph = new Glyph("P", "#f00", ""); 14 | this.type = ActorType.Pedro; 15 | } 16 | 17 | act(): Promise { 18 | let playerPosition = this.game.getPlayerPosition(); 19 | let astar = new Path.AStar(playerPosition.x, playerPosition.y, this.game.mapIsPassable.bind(this.game), { topology: 4 }); 20 | 21 | this.path = []; 22 | astar.compute(this.position.x, this.position.y, this.pathCallback.bind(this)); 23 | this.path.shift(); // remove Pedros position 24 | 25 | if (this.path.length > 0) { 26 | if (!this.game.occupiedByEnemy(this.path[0].x, this.path[0].y)) { 27 | this.position = new Point(this.path[0].x, this.path[0].y); 28 | } 29 | } 30 | 31 | if (this.position.equals(playerPosition)) { 32 | this.game.catchPlayer(this); 33 | } 34 | 35 | return Promise.resolve(); 36 | } 37 | 38 | private pathCallback(x: number, y: number): void { 39 | this.path.push(new Point(x, y)); 40 | } 41 | } -------------------------------------------------------------------------------- /src/player.ts: -------------------------------------------------------------------------------- 1 | import { KEYS, DIRS } from "rot-js"; 2 | import { Game } from "./game"; 3 | import { Actor, ActorType } from "./actor"; 4 | import { Point } from "./point"; 5 | import { Glyph } from "./glyph"; 6 | import { InputUtility } from "./input-utility"; 7 | 8 | export class Player implements Actor { 9 | glyph: Glyph; 10 | type: ActorType; 11 | private keyMap: { [key: number]: number } 12 | 13 | constructor(private game: Game, public position: Point) { 14 | this.glyph = new Glyph("@", "#ff0"); 15 | this.type = ActorType.Player; 16 | 17 | this.keyMap = {}; 18 | this.keyMap[KEYS.VK_NUMPAD8] = 0; // up 19 | this.keyMap[KEYS.VK_NUMPAD9] = 1; 20 | this.keyMap[KEYS.VK_NUMPAD6] = 2; // right 21 | this.keyMap[KEYS.VK_NUMPAD3] = 3; 22 | this.keyMap[KEYS.VK_NUMPAD2] = 4; // down 23 | this.keyMap[KEYS.VK_NUMPAD1] = 5; 24 | this.keyMap[KEYS.VK_NUMPAD4] = 6; // left 25 | this.keyMap[KEYS.VK_NUMPAD7] = 7; 26 | } 27 | 28 | act(): Promise { 29 | return InputUtility.waitForInput(this.handleInput.bind(this)); 30 | } 31 | 32 | private handleInput(event: KeyboardEvent): boolean { 33 | let validInput = false; 34 | let code = event.keyCode; 35 | if (code in this.keyMap) { 36 | let diff = DIRS[8][this.keyMap[code]]; 37 | let newPoint = new Point(this.position.x + diff[0], this.position.y + diff[1]); 38 | if (!this.game.mapIsPassable(newPoint.x, newPoint.y)) { 39 | return; 40 | } 41 | this.position = newPoint; 42 | validInput = true; 43 | } else if (code === KEYS.VK_RETURN || code === KEYS.VK_SPACE) { 44 | this.game.checkBox(this.position.x, this.position.y); 45 | validInput = true; 46 | } else { 47 | validInput = code === KEYS.VK_NUMPAD5; // Wait a turn 48 | } 49 | return validInput; 50 | } 51 | } -------------------------------------------------------------------------------- /src/tiny-pedro.ts: -------------------------------------------------------------------------------- 1 | import { Path } from "rot-js"; 2 | import { Game } from "./game"; 3 | import { Actor, ActorType } from "./actor"; 4 | import { Point } from "./point"; 5 | import { Glyph } from "./glyph"; 6 | import { Tile } from "./tile"; 7 | 8 | export class TinyPedro implements Actor { 9 | glyph: Glyph; 10 | type: ActorType; 11 | private target: Point; 12 | private path: Point[]; 13 | 14 | constructor(private game: Game, public position: Point) { 15 | this.glyph = new Glyph("p", "#00f", ""); 16 | this.type = ActorType.TinyPedro; 17 | } 18 | 19 | act(): Promise { 20 | let playerPosition = this.game.getPlayerPosition(); 21 | if (this.position.equals(playerPosition)) { 22 | this.game.catchPlayer(this); 23 | } 24 | 25 | if (!this.target || this.game.getTileType(this.target.x, this.target.y) != Tile.box.type) { 26 | this.target = this.game.getRandomTilePositions(Tile.box.type)[0]; 27 | } 28 | let astar = new Path.AStar(this.target.x, this.target.y, this.game.mapIsPassable.bind(this.game), { topology: 8 }); 29 | 30 | this.path = []; 31 | astar.compute(this.position.x, this.position.y, this.pathCallback.bind(this)); 32 | this.path.shift(); // remove tiny Pedros position 33 | 34 | if (this.path.length > 0) { 35 | if (!this.game.occupiedByEnemy(this.path[0].x, this.path[0].y)) { 36 | this.position = new Point(this.path[0].x, this.path[0].y); 37 | } 38 | } 39 | 40 | if (this.position.equals(playerPosition)) { 41 | this.game.catchPlayer(this); 42 | } else if (this.position.equals(this.target)) { 43 | this.game.destroyBox(this, this.target.x, this.target.y); 44 | this.target = undefined; 45 | } 46 | 47 | return Promise.resolve(); 48 | } 49 | 50 | private pathCallback(x: number, y: number): void { 51 | this.path.push(new Point(x, y)); 52 | } 53 | } -------------------------------------------------------------------------------- /src/map.ts: -------------------------------------------------------------------------------- 1 | import { Map as RotJsMap } from "rot-js/lib/index"; 2 | import { RNG } from "rot-js"; 3 | import { Game } from "./game"; 4 | import { Tile, TileType } from "./tile"; 5 | import { Point } from "./point"; 6 | 7 | export class Map { 8 | private map: { [key: string]: Tile }; 9 | 10 | constructor(private game: Game) { 11 | this.map = {}; 12 | } 13 | 14 | generateMap(width: number, height: number): void { 15 | this.map = {}; 16 | let digger = new RotJsMap.Digger(width, height); 17 | digger.create(this.diggerCallback.bind(this)); 18 | } 19 | 20 | setTile(x: number, y: number, tile: Tile): void { 21 | this.map[this.coordinatesToKey(x, y)] = tile; 22 | } 23 | 24 | getRandomTilePositions(type: TileType, quantity: number = 1): Point[] { 25 | let buffer: Point[] = []; 26 | let result: Point[] = []; 27 | for (let key in this.map) { 28 | if (this.map[key].type === type) { 29 | buffer.push(this.keyToPoint(key)); 30 | } 31 | } 32 | 33 | let index: number; 34 | while (buffer.length > 0 && result.length < quantity) { 35 | index = Math.floor(RNG.getUniform() * buffer.length); 36 | result.push(buffer.splice(index, 1)[0]); 37 | } 38 | return result; 39 | } 40 | 41 | getTileType(x: number, y: number): TileType { 42 | return this.map[this.coordinatesToKey(x, y)].type; 43 | } 44 | 45 | isPassable(x: number, y: number): boolean { 46 | return this.coordinatesToKey(x, y) in this.map; 47 | } 48 | 49 | draw(): void { 50 | for (let key in this.map) { 51 | this.game.draw(this.keyToPoint(key), this.map[key].glyph); 52 | } 53 | } 54 | 55 | private coordinatesToKey(x: number, y: number): string { 56 | return x + "," + y; 57 | } 58 | 59 | private keyToPoint(key: string): Point { 60 | let parts = key.split(","); 61 | return new Point(parseInt(parts[0]), parseInt(parts[1])); 62 | } 63 | 64 | private diggerCallback(x: number, y: number, wall: number): void { 65 | if (wall) { 66 | return; 67 | } 68 | this.map[this.coordinatesToKey(x, y)] = Tile.floor; 69 | } 70 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rot.js TypeScript basics 2 | 3 | A basic roguelike example built with rot.js and TypeScript. Playable at [https://mizar999.github.io/rotjs-typescript-basics/](https://mizar999.github.io/rotjs-typescript-basics/) 4 | 5 | ## Resources 6 | 7 | - [rot.js - Roguelike Toolkit](https://github.com/ondras/rot.js) 8 | - [RogueBasin - rot.js Tutorial](http://www.roguebasin.roguelikedevelopment.org/index.php?title=Rot.js_tutorial) 9 | - [Frostlike - 7 Day Roguelike Challenge 2017 entry](https://github.com/maqqr/7drl2017) 10 | 11 | ## How to run 12 | 13 | After cloning the repository: 14 | 15 | - Install necessary packages 16 | 17 | ```powershell 18 | npm install 19 | ``` 20 | 21 | - To build the application run: 22 | 23 | ```powershell 24 | npm run build 25 | ``` 26 | 27 | - To run multiple npm scripts cross platform in parallel run the following command: 28 | 29 | ```powershell 30 | # if globally installed 31 | concurrently npm:watch npm:serve 32 | 33 | # if locally installed 34 | npx concurrently npm:watch npm:serve 35 | ``` 36 | 37 | ## Initial Project setup 38 | 39 | If you're interested here is my initial project setup: 40 | 41 | - Init npm and install necessary packages 42 | 43 | ```powershell 44 | npm init -y 45 | npm install --save-dev typescript@4.9.4 ts-loader@9.4.2 rot-js@2.2.0 webpack@5.75.0 webpack-cli@5.0.1 http-server@14.1.1 concurrently@7.6.0 46 | ``` 47 | 48 | - Create **Webpack** configuration `webpack.config.js`: 49 | 50 | ```javascript 51 | const path = require('path'); 52 | 53 | module.exports = { 54 | entry: './src/app.ts', 55 | module: { 56 | rules:[{ 57 | test: /\.tsx?$/, 58 | use: 'ts-loader', 59 | exclude: /node_modules/ 60 | }] 61 | }, 62 | resolve: { 63 | extensions: ['.ts', '.tsx', '.js'] 64 | }, 65 | output: { 66 | filename: 'app.js', 67 | path: path.resolve(__dirname, 'dist') 68 | }, 69 | mode: 'development' 70 | }; 71 | ``` 72 | 73 | - Webpack will get the sources from `src/app.ts` and collect everything in `dist/app.js` file 74 | - Create **TypeScript** configuration `tsconfig.json`: 75 | 76 | ```json 77 | { 78 | "compilerOptions": { 79 | "target": "es5" 80 | }, 81 | "include": [ 82 | "src/*" 83 | ] 84 | } 85 | ``` 86 | 87 | - Update the **scripts**-section of the `package.json` file: 88 | 89 | ```json 90 | "scripts": { 91 | "build": "webpack", 92 | "watch": "webpack --watch", 93 | "serve": "http-server --port=8085 -c-1" 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /src/game.ts: -------------------------------------------------------------------------------- 1 | import { Display, Scheduler, KEYS, RNG } from "rot-js/lib/index"; 2 | import Simple from "rot-js/lib/scheduler/simple"; 3 | 4 | import { Player } from "./player"; 5 | import { Point } from "./point"; 6 | import { Glyph } from "./glyph"; 7 | import { Actor, ActorType } from "./actor"; 8 | import { Pedro } from "./pedro"; 9 | import { GameState } from "./game-state"; 10 | import { StatusLine } from "./status-line"; 11 | import { MessageLog } from "./message-log"; 12 | import { InputUtility } from "./input-utility"; 13 | import { Tile, TileType } from "./tile"; 14 | import { Map } from "./map"; 15 | import { TinyPedro } from "./tiny-pedro"; 16 | 17 | export class Game { 18 | private display: Display; 19 | private scheduler: Simple; 20 | private map: Map; 21 | private statusLine: StatusLine; 22 | private messageLog: MessageLog; 23 | 24 | private player: Player; 25 | private enemies: Actor[]; 26 | 27 | private gameSize: { width: number, height: number }; 28 | private mapSize: { width: number, height: number }; 29 | private statusLinePosition: Point; 30 | private actionLogPosition: Point; 31 | private gameState: GameState; 32 | 33 | private pineapplePoint: Point; 34 | private pedroColor: string; 35 | private foregroundColor = "white"; 36 | private backgroundColor = "black"; 37 | private maximumBoxes = 10; 38 | 39 | constructor() { 40 | this.gameSize = { width: 75, height: 25 }; 41 | this.mapSize = { width: this.gameSize.width, height: this.gameSize.height - 4 }; 42 | this.statusLinePosition = new Point(0, this.gameSize.height - 4); 43 | this.actionLogPosition = new Point(0, this.gameSize.height - 3); 44 | 45 | this.display = new Display({ 46 | width: this.gameSize.width, 47 | height: this.gameSize.height, 48 | fontSize: 20 49 | }); 50 | document.body.appendChild(this.display.getContainer()); 51 | 52 | this.gameState = new GameState(); 53 | this.map = new Map(this); 54 | this.statusLine = new StatusLine(this, this.statusLinePosition, this.gameSize.width, { maxBoxes: this.maximumBoxes }); 55 | this.messageLog = new MessageLog(this, this.actionLogPosition, this.gameSize.width, 3); 56 | this.pedroColor = new Pedro(this, new Point(0, 0)).glyph.foregroundColor; 57 | 58 | this.initializeGame(); 59 | this.mainLoop(); 60 | } 61 | 62 | draw(position: Point, glyph: Glyph): void { 63 | let foreground = glyph.foregroundColor || this.foregroundColor; 64 | let background = glyph.backgroundColor || this.backgroundColor; 65 | this.display.draw(position.x, position.y, glyph.character, foreground, background); 66 | } 67 | 68 | drawText(position: Point, text: string, maxWidth?: number): void { 69 | this.display.drawText(position.x, position.y, text, maxWidth); 70 | } 71 | 72 | mapIsPassable(x: number, y: number): boolean { 73 | return this.map.isPassable(x, y); 74 | } 75 | 76 | occupiedByEnemy(x: number, y: number): boolean { 77 | for (let enemy of this.enemies) { 78 | if (enemy.position.x == x && enemy.position.y == y) { 79 | return true; 80 | } 81 | } 82 | return false; 83 | } 84 | 85 | getPlayerPosition(): Point { 86 | return this.player.position; 87 | } 88 | 89 | checkBox(x: number, y: number): void { 90 | switch (this.map.getTileType(x, y)) { 91 | case Tile.box.type: 92 | this.map.setTile(x, y, Tile.searchedBox); 93 | this.statusLine.boxes += 1; 94 | if (this.pineapplePoint.x == x && this.pineapplePoint.y == y) { 95 | this.messageLog.appendText("Continue with 'spacebar' or 'return'."); 96 | this.messageLog.appendText("Hooray! You found a pineapple."); 97 | this.gameState.foundPineapple = true; 98 | } else { 99 | this.messageLog.appendText("This box is empty."); 100 | } 101 | break; 102 | case Tile.searchedBox.type: 103 | this.map.setTile(x, y, Tile.destroyedBox); 104 | this.messageLog.appendText("You destroy this box!"); 105 | break; 106 | case Tile.destroyedBox.type: 107 | this.messageLog.appendText("This box is already destroyed."); 108 | break; 109 | default: 110 | this.messageLog.appendText("There is no box here!"); 111 | break; 112 | } 113 | } 114 | 115 | destroyBox(actor: Actor, x: number, y: number): void { 116 | switch (this.map.getTileType(x, y)) { 117 | case TileType.Box: 118 | case TileType.SearchedBox: 119 | this.map.setTile(x, y, Tile.destroyedBox); 120 | if (this.pineapplePoint.x == x && this.pineapplePoint.y == y) { 121 | this.messageLog.appendText("Continue with 'spacebar' or 'return'."); 122 | this.messageLog.appendText(`Game over - ${this.getActorName(actor)} detroyed the box with the pineapple.`); 123 | this.gameState.pineappleWasDestroyed = true; 124 | } else { 125 | this.messageLog.appendText(`${this.getActorName(actor)} detroyed a box.`); 126 | } 127 | break; 128 | case TileType.DestroyedBox: 129 | this.messageLog.appendText("This box is already destroyed."); 130 | break; 131 | default: 132 | this.messageLog.appendText("There is no box here!"); 133 | break; 134 | } 135 | } 136 | 137 | catchPlayer(actor: Actor): void { 138 | this.messageLog.appendText("Continue with 'spacebar' or 'return'."); 139 | this.messageLog.appendText(`Game over - you were captured by ${this.getActorName(actor)}!`); 140 | this.gameState.playerWasCaught = true; 141 | } 142 | 143 | getTileType(x: number, y: number): TileType { 144 | return this.map.getTileType(x, y); 145 | } 146 | 147 | getRandomTilePositions(type: TileType, quantity: number = 1): Point[] { 148 | return this.map.getRandomTilePositions(type, quantity); 149 | } 150 | 151 | private initializeGame(): void { 152 | this.display.clear(); 153 | 154 | this.messageLog.clear(); 155 | if (!this.gameState.isGameOver() || this.gameState.doRestartGame()) { 156 | this.resetStatusLine(); 157 | this.writeHelpMessage(); 158 | } else { 159 | this.statusLine.boxes = 0; 160 | } 161 | this.gameState.reset(); 162 | 163 | this.map.generateMap(this.mapSize.width, this.mapSize.height); 164 | this.generateBoxes(); 165 | 166 | this.createBeings(); 167 | this.scheduler = new Scheduler.Simple(); 168 | this.scheduler.add(this.player, true); 169 | for (let enemy of this.enemies) { 170 | this.scheduler.add(enemy, true); 171 | } 172 | 173 | this.drawPanel(); 174 | } 175 | 176 | private async mainLoop(): Promise { 177 | let actor: Actor; 178 | while (true) { 179 | actor = this.scheduler.next(); 180 | if (!actor) { 181 | break; 182 | } 183 | 184 | await actor.act(); 185 | if (actor.type === ActorType.Player) { 186 | this.statusLine.turns += 1; 187 | } 188 | if (this.gameState.foundPineapple) { 189 | this.statusLine.pineapples += 1; 190 | } 191 | 192 | this.drawPanel(); 193 | 194 | if (this.gameState.isGameOver()) { 195 | await InputUtility.waitForInput(this.handleInput.bind(this)); 196 | this.initializeGame(); 197 | } 198 | } 199 | } 200 | 201 | private drawPanel(): void { 202 | this.display.clear(); 203 | this.map.draw(); 204 | this.statusLine.draw(); 205 | this.messageLog.draw(); 206 | this.draw(this.player.position, this.player.glyph); 207 | for (let enemy of this.enemies) { 208 | this.draw(enemy.position, enemy.glyph); 209 | } 210 | } 211 | 212 | private handleInput(event: KeyboardEvent): boolean { 213 | let code = event.keyCode; 214 | return code === KEYS.VK_SPACE || code === KEYS.VK_RETURN; 215 | } 216 | 217 | private writeHelpMessage(): void { 218 | let helpMessage = [ 219 | `Find the pineapple in one of the %c{${Tile.box.glyph.foregroundColor}}boxes%c{}.`, 220 | `Move with numpad, search %c{${Tile.box.glyph.foregroundColor}}box%c{} with 'spacebar' or 'return'.`, 221 | `Watch out for %c{${this.pedroColor}}Pedro%c{}!` 222 | ]; 223 | 224 | for (let index = helpMessage.length - 1; index >= 0; --index) { 225 | this.messageLog.appendText(helpMessage[index]); 226 | } 227 | } 228 | 229 | private getActorName(actor: Actor): string { 230 | switch (actor.type) { 231 | case ActorType.Player: 232 | return `Player`; 233 | case ActorType.Pedro: 234 | return `%c{${actor.glyph.foregroundColor}}Pedro%c{}`; 235 | case ActorType.TinyPedro: 236 | return `%c{${actor.glyph.foregroundColor}}Pedros son%c{}`; 237 | default: 238 | return "unknown actor"; 239 | } 240 | } 241 | 242 | private generateBoxes(): void { 243 | let positions = this.map.getRandomTilePositions(TileType.Floor, this.maximumBoxes); 244 | for (let position of positions) { 245 | this.map.setTile(position.x, position.y, Tile.box); 246 | } 247 | this.pineapplePoint = positions[0]; 248 | } 249 | 250 | private createBeings(): void { 251 | let numberOfEnemies = 1 + Math.floor(this.statusLine.pineapples / 3.0); 252 | this.enemies = []; 253 | let positions = this.map.getRandomTilePositions(TileType.Floor, 1 + numberOfEnemies); 254 | this.player = new Player(this, positions.splice(0, 1)[0]); 255 | for (let position of positions) { 256 | if (this.statusLine.pineapples < 1 || RNG.getUniform() < 0.5) { 257 | this.enemies.push(new Pedro(this, position)); 258 | } else { 259 | this.enemies.push(new TinyPedro(this, position)); 260 | } 261 | } 262 | } 263 | 264 | private resetStatusLine(): void { 265 | this.statusLine.reset(); 266 | this.statusLine.maxBoxes = this.maximumBoxes; 267 | } 268 | } --------------------------------------------------------------------------------