
├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── scripts │ ├── characters │ │ ├── ghost.js │ │ └── pacman.js │ ├── core │ │ ├── gameCoordinator.js │ │ └── gameEngine.js │ ├── pickups │ │ └── pickup.js │ └── utilities │ │ ├── characterUtil.js │ │ ├── soundManager.js │ │ └── timer.js ├── style │ ├── audio │ │ ├── death.mp3 │ │ ├── dot_1.mp3 │ │ ├── dot_2.mp3 │ │ ├── eat_ghost.mp3 │ │ ├── extra_life.mp3 │ │ ├── eyes.mp3 │ │ ├── fruit.mp3 │ │ ├── game_start.mp3 │ │ ├── pause.mp3 │ │ ├── pause_beat.mp3 │ │ ├── power_up.mp3 │ │ ├── siren_1.mp3 │ │ ├── siren_2.mp3 │ │ └── siren_3.mp3 │ ├── graphics │ │ ├── backdrop.png │ │ ├── brent_ward_logo.png │ │ ├── brent_ward_logo.svg │ │ ├── brent_ward_logo_black.png │ │ ├── brent_ward_logo_black.svg │ │ ├── extra_life.png │ │ ├── extra_life.svg │ │ ├── pacman_logo.png │ │ └── spriteSheets │ │ │ ├── characters │ │ │ ├── ghosts │ │ │ │ ├── blinky │ │ │ │ │ ├── blinky_down.png │ │ │ │ │ ├── blinky_down.svg │ │ │ │ │ ├── blinky_down_angry.png │ │ │ │ │ ├── blinky_down_angry.svg │ │ │ │ │ ├── blinky_down_annoyed.png │ │ │ │ │ ├── blinky_down_annoyed.svg │ │ │ │ │ ├── blinky_left.png │ │ │ │ │ ├── blinky_left.svg │ │ │ │ │ ├── blinky_left_angry.png │ │ │ │ │ ├── blinky_left_angry.svg │ │ │ │ │ ├── blinky_left_annoyed.png │ │ │ │ │ ├── blinky_left_annoyed.svg │ │ │ │ │ ├── blinky_right.png │ │ │ │ │ ├── blinky_right.svg │ │ │ │ │ ├── blinky_right_angry.png │ │ │ │ │ ├── blinky_right_angry.svg │ │ │ │ │ ├── blinky_right_annoyed.png │ │ │ │ │ ├── blinky_right_annoyed.svg │ │ │ │ │ ├── blinky_up.png │ │ │ │ │ ├── blinky_up.svg │ │ │ │ │ ├── blinky_up_angry.png │ │ │ │ │ ├── blinky_up_angry.svg │ │ │ │ │ ├── blinky_up_annoyed.png │ │ │ │ │ └── blinky_up_annoyed.svg │ │ │ │ ├── clyde │ │ │ │ │ ├── clyde_down.png │ │ │ │ │ ├── clyde_down.svg │ │ │ │ │ ├── clyde_left.png │ │ │ │ │ ├── clyde_left.svg │ │ │ │ │ ├── clyde_right.png │ │ │ │ │ ├── clyde_right.svg │ │ │ │ │ ├── clyde_up.png │ │ │ │ │ └── clyde_up.svg │ │ │ │ ├── eyes_down.png │ │ │ │ ├── eyes_down.svg │ │ │ │ ├── eyes_left.png │ │ │ │ ├── eyes_left.svg │ │ │ │ ├── eyes_right.png │ │ │ │ ├── eyes_right.svg │ │ │ │ ├── eyes_up.png │ │ │ │ ├── eyes_up.svg │ │ │ │ ├── inky │ │ │ │ │ ├── inky_down.png │ │ │ │ │ ├── inky_down.svg │ │ │ │ │ ├── inky_left.png │ │ │ │ │ ├── inky_left.svg │ │ │ │ │ ├── inky_right.png │ │ │ │ │ ├── inky_right.svg │ │ │ │ │ ├── inky_up.png │ │ │ │ │ └── inky_up.svg │ │ │ │ ├── pinky │ │ │ │ │ ├── pinky_down.png │ │ │ │ │ ├── pinky_down.svg │ │ │ │ │ ├── pinky_left.png │ │ │ │ │ ├── pinky_left.svg │ │ │ │ │ ├── pinky_right.png │ │ │ │ │ ├── pinky_right.svg │ │ │ │ │ ├── pinky_up.png │ │ │ │ │ └── pinky_up.svg │ │ │ │ ├── scared_blue.png │ │ │ │ ├── scared_blue.svg │ │ │ │ ├── scared_white.png │ │ │ │ └── scared_white.svg │ │ │ └── pacman │ │ │ │ ├── arrow_down.png │ │ │ │ ├── arrow_down.svg │ │ │ │ ├── arrow_left.png │ │ │ │ ├── arrow_left.svg │ │ │ │ ├── arrow_right.png │ │ │ │ ├── arrow_right.svg │ │ │ │ ├── arrow_up.png │ │ │ │ ├── arrow_up.svg │ │ │ │ ├── pacman_death.png │ │ │ │ ├── pacman_death.svg │ │ │ │ ├── pacman_down.png │ │ │ │ ├── pacman_down.svg │ │ │ │ ├── pacman_error.png │ │ │ │ ├── pacman_error.svg │ │ │ │ ├── pacman_left.png │ │ │ │ ├── pacman_left.svg │ │ │ │ ├── pacman_right.png │ │ │ │ ├── pacman_right.svg │ │ │ │ ├── pacman_up.png │ │ │ │ └── pacman_up.svg │ │ │ ├── maze │ │ │ ├── maze_blue.png │ │ │ ├── maze_blue.svg │ │ │ ├── maze_white.png │ │ │ └── maze_white.svg │ │ │ ├── pickups │ │ │ ├── apple.png │ │ │ ├── apple.svg │ │ │ ├── bell.png │ │ │ ├── bell.svg │ │ │ ├── cherry.png │ │ │ ├── cherry.svg │ │ │ ├── galaxian.png │ │ │ ├── galaxian.svg │ │ │ ├── key.png │ │ │ ├── key.svg │ │ │ ├── melon.png │ │ │ ├── melon.svg │ │ │ ├── orange.png │ │ │ ├── orange.svg │ │ │ ├── pacdot.png │ │ │ ├── pacdot.svg │ │ │ ├── powerPellet.png │ │ │ ├── powerPellet.svg │ │ │ ├── strawberry.png │ │ │ └── strawberry.svg │ │ │ ├── references │ │ │ ├── characterSpriteReference.png │ │ │ ├── inky_target.png │ │ │ ├── mazeGridSystemReference.png │ │ │ └── mazeSpriteReference.png │ │ │ └── text │ │ │ ├── 100.png │ │ │ ├── 100.svg │ │ │ ├── 1000.png │ │ │ ├── 1000.svg │ │ │ ├── 1600.png │ │ │ ├── 1600.svg │ │ │ ├── 200.png │ │ │ ├── 200.svg │ │ │ ├── 2000.png │ │ │ ├── 2000.svg │ │ │ ├── 300.png │ │ │ ├── 300.svg │ │ │ ├── 3000.png │ │ │ ├── 3000.svg │ │ │ ├── 400.png │ │ │ ├── 400.svg │ │ │ ├── 500.png │ │ │ ├── 500.svg │ │ │ ├── 5000.png │ │ │ ├── 5000.svg │ │ │ ├── 700.png │ │ │ ├── 700.svg │ │ │ ├── 800.png │ │ │ ├── 800.svg │ │ │ ├── game_over.png │ │ │ ├── game_over.svg │ │ │ ├── ready.png │ │ │ └── ready.svg │ └── scss │ │ ├── _variables.scss │ │ ├── ghosts.scss │ │ ├── mainPage.scss │ │ ├── maze.scss │ │ ├── pacman.scss │ │ └── pickups.scss └── tests │ ├── .eslintrc.js │ ├── characterUtil.test.js │ ├── gameCoordinator.test.js │ ├── gameEngine.test.js │ ├── ghost.test.js │ ├── pacman.test.js │ ├── pickup.test.js │ ├── soundManager.test.js │ └── timer.test.js ├── build ├── app.css └── app.js ├── favicon.ico ├── gulpfile.js ├── index.html ├── package-lock.json └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | build/* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb-base", 3 | "rules": { 4 | "no-undef": "off", 5 | "class-methods-use-this": "off", 6 | "linebreak-style": "off", 7 | "no-new": "off", 8 | "max-len": ["error", { "comments": 100 }] 9 | } 10 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.nyc_output -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npm test 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | install: 5 | - npm i 6 | script: 7 | - gulp 8 | - npm test 9 | after_success: 10 | - npm run coverage 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brent Ward 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pacman-js 2 | 3 | [](https://travis-ci.com/bward2/pacman-js) 4 | [](https://coveralls.io/github/bward2/pacman-js?branch=master) 5 | [](https://github.com/airbnb/javascript) 6 | 7 | Pacman clone made with Javascript, HTML, and CSS. 8 | 9 | ### _**[Play it!](https://bward2.github.io/pacman-js/)**_ 10 | 11 | 🍒🍓🍊🍎🍈👾🔔🔑 12 | 13 | ## Development Instructions 14 | 15 | This project makes use of _**[NodeJS](https://nodejs.org/en/)**_. Download it, then clone this repo and run the following commands: 16 | 17 | 1. `npm i` (Installs necessary packages for development) 18 | 1. `npm run watch` (Watches changes to JS and SCSS files for continuous compilation) 19 | 1. `npm run serve` (Hosts the files locally) 20 | 21 | The game can now be accessed at _**http://127.0.0.1:8080/index**_ 22 | 23 | This project also utilizes _**[Husky](https://github.com/typicode/husky)**_ to enforce best coding practices. The current thresholds are 0 linting errors upon commits (following Airbnb's standard), and 100% unit test code coverage upon pushes. 24 | 25 | Feel free to submit PRs and/or report any issues you find! 😃 26 | -------------------------------------------------------------------------------- /app/scripts/characters/pacman.js: -------------------------------------------------------------------------------- 1 | class Pacman { 2 | constructor(scaledTileSize, mazeArray, characterUtil) { 3 | this.scaledTileSize = scaledTileSize; 4 | this.mazeArray = mazeArray; 5 | this.characterUtil = characterUtil; 6 | this.animationTarget = document.getElementById('pacman'); 7 | this.pacmanArrow = document.getElementById('pacman-arrow'); 8 | 9 | this.reset(); 10 | } 11 | 12 | /** 13 | * Rests the character to its default state 14 | */ 15 | reset() { 16 | this.setMovementStats(this.scaledTileSize); 17 | this.setSpriteAnimationStats(); 18 | this.setStyleMeasurements(this.scaledTileSize, this.spriteFrames); 19 | this.setDefaultPosition(this.scaledTileSize); 20 | this.setSpriteSheet(this.direction); 21 | this.pacmanArrow.style.backgroundImage = 'url(app/style/graphics/' 22 | + `spriteSheets/characters/pacman/arrow_${this.direction}.svg)`; 23 | } 24 | 25 | /** 26 | * Sets various properties related to Pacman's movement 27 | * @param {number} scaledTileSize - The dimensions of a single tile 28 | */ 29 | setMovementStats(scaledTileSize) { 30 | this.velocityPerMs = this.calculateVelocityPerMs(scaledTileSize); 31 | this.desiredDirection = this.characterUtil.directions.left; 32 | this.direction = this.characterUtil.directions.left; 33 | this.moving = false; 34 | } 35 | 36 | /** 37 | * Sets values pertaining to Pacman's spritesheet animation 38 | */ 39 | setSpriteAnimationStats() { 40 | this.specialAnimation = false; 41 | this.display = true; 42 | this.animate = true; 43 | this.loopAnimation = true; 44 | this.msBetweenSprites = 50; 45 | this.msSinceLastSprite = 0; 46 | this.spriteFrames = 4; 47 | this.backgroundOffsetPixels = 0; 48 | this.animationTarget.style.backgroundPosition = '0px 0px'; 49 | } 50 | 51 | /** 52 | * Sets css property values for Pacman and Pacman's Arrow 53 | * @param {number} scaledTileSize - The dimensions of a single tile 54 | * @param {number} spriteFrames - The number of frames in Pacman's spritesheet 55 | */ 56 | setStyleMeasurements(scaledTileSize, spriteFrames) { 57 | this.measurement = scaledTileSize * 2; 58 | 59 | this.animationTarget.style.height = `${this.measurement}px`; 60 | this.animationTarget.style.width = `${this.measurement}px`; 61 | this.animationTarget.style.backgroundSize = `${ 62 | this.measurement * spriteFrames 63 | }px`; 64 | 65 | this.pacmanArrow.style.height = `${this.measurement * 2}px`; 66 | this.pacmanArrow.style.width = `${this.measurement * 2}px`; 67 | this.pacmanArrow.style.backgroundSize = `${this.measurement * 2}px`; 68 | } 69 | 70 | /** 71 | * Sets the default position and direction for Pacman at the game's start 72 | * @param {number} scaledTileSize - The dimensions of a single tile 73 | */ 74 | setDefaultPosition(scaledTileSize) { 75 | this.defaultPosition = { 76 | top: scaledTileSize * 22.5, 77 | left: scaledTileSize * 13, 78 | }; 79 | this.position = Object.assign({}, this.defaultPosition); 80 | this.oldPosition = Object.assign({}, this.position); 81 | this.animationTarget.style.top = `${this.position.top}px`; 82 | this.animationTarget.style.left = `${this.position.left}px`; 83 | } 84 | 85 | /** 86 | * Calculates how fast Pacman should move in a millisecond 87 | * @param {number} scaledTileSize - The dimensions of a single tile 88 | */ 89 | calculateVelocityPerMs(scaledTileSize) { 90 | // In the original game, Pacman moved at 11 tiles per second. 91 | const velocityPerSecond = scaledTileSize * 11; 92 | return velocityPerSecond / 1000; 93 | } 94 | 95 | /** 96 | * Chooses a movement Spritesheet depending upon direction 97 | * @param {('up'|'down'|'left'|'right')} direction - The character's current travel orientation 98 | */ 99 | setSpriteSheet(direction) { 100 | this.animationTarget.style.backgroundImage = 'url(app/style/graphics/' 101 | + `spriteSheets/characters/pacman/pacman_${direction}.svg)`; 102 | } 103 | 104 | prepDeathAnimation() { 105 | this.loopAnimation = false; 106 | this.msBetweenSprites = 125; 107 | this.spriteFrames = 12; 108 | this.specialAnimation = true; 109 | this.backgroundOffsetPixels = 0; 110 | const bgSize = this.measurement * this.spriteFrames; 111 | this.animationTarget.style.backgroundSize = `${bgSize}px`; 112 | this.animationTarget.style.backgroundImage = 'url(app/style/' 113 | + 'graphics/spriteSheets/characters/pacman/pacman_death.svg)'; 114 | this.animationTarget.style.backgroundPosition = '0px 0px'; 115 | this.pacmanArrow.style.backgroundImage = ''; 116 | } 117 | 118 | /** 119 | * Changes Pacman's desiredDirection, updates the PacmanArrow sprite, and sets moving to true 120 | * @param {Event} e - The keydown event to evaluate 121 | * @param {Boolean} startMoving - If true, Pacman will move upon key press 122 | */ 123 | changeDirection(newDirection, startMoving) { 124 | this.desiredDirection = newDirection; 125 | this.pacmanArrow.style.backgroundImage = 'url(app/style/graphics/' 126 | + `spriteSheets/characters/pacman/arrow_${this.desiredDirection}.svg)`; 127 | 128 | if (startMoving) { 129 | this.moving = true; 130 | } 131 | } 132 | 133 | /** 134 | * Updates the position of the leading arrow in front of Pacman 135 | * @param {({top: number, left: number})} position - Pacman's position during the current frame 136 | * @param {number} scaledTileSize - The dimensions of a single tile 137 | */ 138 | updatePacmanArrowPosition(position, scaledTileSize) { 139 | this.pacmanArrow.style.top = `${position.top - scaledTileSize}px`; 140 | this.pacmanArrow.style.left = `${position.left - scaledTileSize}px`; 141 | } 142 | 143 | /** 144 | * Handle Pacman's movement when he is snapped to the x-y grid of the Maze Array 145 | * @param {number} elapsedMs - The amount of MS that have passed since the last update 146 | * @returns {({ top: number, left: number})} 147 | */ 148 | handleSnappedMovement(elapsedMs) { 149 | const desired = this.characterUtil.determineNewPositions( 150 | this.position, this.desiredDirection, this.velocityPerMs, 151 | elapsedMs, this.scaledTileSize, 152 | ); 153 | const alternate = this.characterUtil.determineNewPositions( 154 | this.position, this.direction, this.velocityPerMs, 155 | elapsedMs, this.scaledTileSize, 156 | ); 157 | 158 | if (this.characterUtil.checkForWallCollision( 159 | desired.newGridPosition, this.mazeArray, this.desiredDirection, 160 | )) { 161 | if (this.characterUtil.checkForWallCollision( 162 | alternate.newGridPosition, this.mazeArray, this.direction, 163 | )) { 164 | this.moving = false; 165 | return this.position; 166 | } 167 | return alternate.newPosition; 168 | } 169 | this.direction = this.desiredDirection; 170 | this.setSpriteSheet(this.direction); 171 | return desired.newPosition; 172 | } 173 | 174 | /** 175 | * Handle Pacman's movement when he is inbetween tiles on the x-y grid of the Maze Array 176 | * @param {({x: number, y: number})} gridPosition - x-y position during the current frame 177 | * @param {number} elapsedMs - The amount of MS that have passed since the last update 178 | * @returns {({ top: number, left: number})} 179 | */ 180 | handleUnsnappedMovement(gridPosition, elapsedMs) { 181 | const desired = this.characterUtil.determineNewPositions( 182 | this.position, this.desiredDirection, this.velocityPerMs, 183 | elapsedMs, this.scaledTileSize, 184 | ); 185 | const alternate = this.characterUtil.determineNewPositions( 186 | this.position, this.direction, this.velocityPerMs, 187 | elapsedMs, this.scaledTileSize, 188 | ); 189 | 190 | if (this.characterUtil.turningAround( 191 | this.direction, this.desiredDirection, 192 | )) { 193 | this.direction = this.desiredDirection; 194 | this.setSpriteSheet(this.direction); 195 | return desired.newPosition; 196 | } if (this.characterUtil.changingGridPosition( 197 | gridPosition, alternate.newGridPosition, 198 | )) { 199 | return this.characterUtil.snapToGrid( 200 | gridPosition, this.direction, this.scaledTileSize, 201 | ); 202 | } 203 | return alternate.newPosition; 204 | } 205 | 206 | /** 207 | * Updates the css position, hides if there is a stutter, and animates the spritesheet 208 | * @param {number} interp - The animation accuracy as a percentage 209 | */ 210 | draw(interp) { 211 | const newTop = this.characterUtil.calculateNewDrawValue( 212 | interp, 'top', this.oldPosition, this.position, 213 | ); 214 | const newLeft = this.characterUtil.calculateNewDrawValue( 215 | interp, 'left', this.oldPosition, this.position, 216 | ); 217 | this.animationTarget.style.top = `${newTop}px`; 218 | this.animationTarget.style.left = `${newLeft}px`; 219 | 220 | this.animationTarget.style.visibility = this.display 221 | ? this.characterUtil.checkForStutter(this.position, this.oldPosition) 222 | : 'hidden'; 223 | this.pacmanArrow.style.visibility = this.animationTarget.style.visibility; 224 | 225 | this.updatePacmanArrowPosition(this.position, this.scaledTileSize); 226 | 227 | const updatedProperties = this.characterUtil.advanceSpriteSheet(this); 228 | this.msSinceLastSprite = updatedProperties.msSinceLastSprite; 229 | this.animationTarget = updatedProperties.animationTarget; 230 | this.backgroundOffsetPixels = updatedProperties.backgroundOffsetPixels; 231 | } 232 | 233 | /** 234 | * Handles movement logic for Pacman 235 | * @param {number} elapsedMs - The amount of MS that have passed since the last update 236 | */ 237 | update(elapsedMs) { 238 | this.oldPosition = Object.assign({}, this.position); 239 | 240 | if (this.moving) { 241 | const gridPosition = this.characterUtil.determineGridPosition( 242 | this.position, this.scaledTileSize, 243 | ); 244 | 245 | if (JSON.stringify(this.position) === JSON.stringify( 246 | this.characterUtil.snapToGrid( 247 | gridPosition, this.direction, this.scaledTileSize, 248 | ), 249 | )) { 250 | this.position = this.handleSnappedMovement(elapsedMs); 251 | } else { 252 | this.position = this.handleUnsnappedMovement(gridPosition, elapsedMs); 253 | } 254 | 255 | this.position = this.characterUtil.handleWarp( 256 | this.position, this.scaledTileSize, this.mazeArray, 257 | ); 258 | } 259 | 260 | if (this.moving || this.specialAnimation) { 261 | this.msSinceLastSprite += elapsedMs; 262 | } 263 | } 264 | } 265 | 266 | // removeIf(production) 267 | module.exports = Pacman; 268 | // endRemoveIf(production) 269 | -------------------------------------------------------------------------------- /app/scripts/core/gameEngine.js: -------------------------------------------------------------------------------- 1 | class GameEngine { 2 | constructor(maxFps, entityList) { 3 | this.fpsDisplay = document.getElementById('fps-display'); 4 | this.elapsedMs = 0; 5 | this.lastFrameTimeMs = 0; 6 | this.entityList = entityList; 7 | this.maxFps = maxFps; 8 | this.timestep = 1000 / this.maxFps; 9 | this.fps = this.maxFps; 10 | this.framesThisSecond = 0; 11 | this.lastFpsUpdate = 0; 12 | this.frameId = 0; 13 | this.running = false; 14 | this.started = false; 15 | } 16 | 17 | /** 18 | * Toggles the paused/running status of the game 19 | * @param {Boolean} running - Whether the game is currently in motion 20 | */ 21 | changePausedState(running) { 22 | if (running) { 23 | this.stop(); 24 | } else { 25 | this.start(); 26 | } 27 | } 28 | 29 | /** 30 | * Updates the on-screen FPS counter once per second 31 | * @param {number} timestamp - The amount of MS which has passed since starting the game engine 32 | */ 33 | updateFpsDisplay(timestamp) { 34 | if (timestamp > this.lastFpsUpdate + 1000) { 35 | this.fps = (this.framesThisSecond + this.fps) / 2; 36 | this.lastFpsUpdate = timestamp; 37 | this.framesThisSecond = 0; 38 | } 39 | this.framesThisSecond += 1; 40 | this.fpsDisplay.textContent = `${Math.round(this.fps)} FPS`; 41 | } 42 | 43 | /** 44 | * Calls the draw function for every member of the entityList 45 | * @param {number} interp - The animation accuracy as a percentage 46 | * @param {Array} entityList - List of entities to be used throughout the game 47 | */ 48 | draw(interp, entityList) { 49 | entityList.forEach((entity) => { 50 | if (typeof entity.draw === 'function') { 51 | entity.draw(interp); 52 | } 53 | }); 54 | } 55 | 56 | /** 57 | * Calls the update function for every member of the entityList 58 | * @param {number} elapsedMs - The amount of MS that have passed since the last update 59 | * @param {Array} entityList - List of entities to be used throughout the game 60 | */ 61 | update(elapsedMs, entityList) { 62 | entityList.forEach((entity) => { 63 | if (typeof entity.update === 'function') { 64 | entity.update(elapsedMs); 65 | } 66 | }); 67 | } 68 | 69 | /** 70 | * In the event that a ton of unsimulated frames pile up, discard all of these frames 71 | * to prevent crashing the game 72 | */ 73 | panic() { 74 | this.elapsedMs = 0; 75 | } 76 | 77 | /** 78 | * Draws an initial frame, resets a few tracking variables related to animation, and calls 79 | * the mainLoop function to start the engine 80 | */ 81 | start() { 82 | if (!this.started) { 83 | this.started = true; 84 | 85 | this.frameId = requestAnimationFrame((firstTimestamp) => { 86 | this.draw(1, []); 87 | this.running = true; 88 | this.lastFrameTimeMs = firstTimestamp; 89 | this.lastFpsUpdate = firstTimestamp; 90 | this.framesThisSecond = 0; 91 | 92 | this.frameId = requestAnimationFrame((timestamp) => { 93 | this.mainLoop(timestamp); 94 | }); 95 | }); 96 | } 97 | } 98 | 99 | /** 100 | * Stops the engine and cancels the current animation frame 101 | */ 102 | stop() { 103 | this.running = false; 104 | this.started = false; 105 | cancelAnimationFrame(this.frameId); 106 | } 107 | 108 | /** 109 | * The loop which will process all necessary frames to update the game's entities 110 | * prior to animating them 111 | */ 112 | processFrames() { 113 | let numUpdateSteps = 0; 114 | while (this.elapsedMs >= this.timestep) { 115 | this.update(this.timestep, this.entityList); 116 | this.elapsedMs -= this.timestep; 117 | numUpdateSteps += 1; 118 | if (numUpdateSteps >= this.maxFps) { 119 | this.panic(); 120 | break; 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * A single cycle of the engine which checks to see if enough time has passed, and, if so, 127 | * will kick off the loops to update and draw the game's entities. 128 | * @param {number} timestamp - The amount of MS which has passed since starting the game engine 129 | */ 130 | engineCycle(timestamp) { 131 | if (timestamp < this.lastFrameTimeMs + (1000 / this.maxFps)) { 132 | this.frameId = requestAnimationFrame((nextTimestamp) => { 133 | this.mainLoop(nextTimestamp); 134 | }); 135 | return; 136 | } 137 | 138 | this.elapsedMs += timestamp - this.lastFrameTimeMs; 139 | this.lastFrameTimeMs = timestamp; 140 | this.updateFpsDisplay(timestamp); 141 | this.processFrames(); 142 | this.draw(this.elapsedMs / this.timestep, this.entityList); 143 | 144 | this.frameId = requestAnimationFrame((nextTimestamp) => { 145 | this.mainLoop(nextTimestamp); 146 | }); 147 | } 148 | 149 | /** 150 | * The endless loop which will kick off engine cycles so long as the game is running 151 | * @param {number} timestamp - The amount of MS which has passed since starting the game engine 152 | */ 153 | mainLoop(timestamp) { 154 | this.engineCycle(timestamp); 155 | } 156 | } 157 | 158 | // removeIf(production) 159 | module.exports = GameEngine; 160 | // endRemoveIf(production) 161 | -------------------------------------------------------------------------------- /app/scripts/pickups/pickup.js: -------------------------------------------------------------------------------- 1 | class Pickup { 2 | constructor(type, scaledTileSize, column, row, pacman, mazeDiv, points) { 3 | this.type = type; 4 | this.pacman = pacman; 5 | this.mazeDiv = mazeDiv; 6 | this.points = points; 7 | this.nearPacman = false; 8 | 9 | this.fruitImages = { 10 | 100: 'cherry', 11 | 300: 'strawberry', 12 | 500: 'orange', 13 | 700: 'apple', 14 | 1000: 'melon', 15 | 2000: 'galaxian', 16 | 3000: 'bell', 17 | 5000: 'key', 18 | }; 19 | 20 | this.setStyleMeasurements(type, scaledTileSize, column, row, points); 21 | } 22 | 23 | /** 24 | * Resets the pickup's visibility 25 | */ 26 | reset() { 27 | this.animationTarget.style.visibility = (this.type === 'fruit') 28 | ? 'hidden' : 'visible'; 29 | } 30 | 31 | /** 32 | * Sets various style measurements for the pickup depending on its type 33 | * @param {('pacdot'|'powerPellet'|'fruit')} type - The classification of pickup 34 | * @param {number} scaledTileSize 35 | * @param {number} column 36 | * @param {number} row 37 | * @param {number} points 38 | */ 39 | setStyleMeasurements(type, scaledTileSize, column, row, points) { 40 | if (type === 'pacdot') { 41 | this.size = scaledTileSize * 0.25; 42 | this.x = (column * scaledTileSize) + ((scaledTileSize / 8) * 3); 43 | this.y = (row * scaledTileSize) + ((scaledTileSize / 8) * 3); 44 | } else if (type === 'powerPellet') { 45 | this.size = scaledTileSize; 46 | this.x = (column * scaledTileSize); 47 | this.y = (row * scaledTileSize); 48 | } else { 49 | this.size = scaledTileSize * 2; 50 | this.x = (column * scaledTileSize) - (scaledTileSize * 0.5); 51 | this.y = (row * scaledTileSize) - (scaledTileSize * 0.5); 52 | } 53 | 54 | this.center = { 55 | x: column * scaledTileSize, 56 | y: row * scaledTileSize, 57 | }; 58 | 59 | this.animationTarget = document.createElement('div'); 60 | this.animationTarget.style.position = 'absolute'; 61 | this.animationTarget.style.backgroundSize = `${this.size}px`; 62 | this.animationTarget.style.backgroundImage = this.determineImage( 63 | type, points, 64 | ); 65 | this.animationTarget.style.height = `${this.size}px`; 66 | this.animationTarget.style.width = `${this.size}px`; 67 | this.animationTarget.style.top = `${this.y}px`; 68 | this.animationTarget.style.left = `${this.x}px`; 69 | this.mazeDiv.appendChild(this.animationTarget); 70 | 71 | if (type === 'powerPellet') { 72 | this.animationTarget.classList.add('power-pellet'); 73 | } 74 | 75 | this.reset(); 76 | } 77 | 78 | /** 79 | * Determines the Pickup image based on type and point value 80 | * @param {('pacdot'|'powerPellet'|'fruit')} type - The classification of pickup 81 | * @param {Number} points 82 | * @returns {String} 83 | */ 84 | determineImage(type, points) { 85 | let image = ''; 86 | 87 | if (type === 'fruit') { 88 | image = this.fruitImages[points] || 'cherry'; 89 | } else { 90 | image = type; 91 | } 92 | 93 | return `url(app/style/graphics/spriteSheets/pickups/${image}.svg)`; 94 | } 95 | 96 | /** 97 | * Shows a bonus fruit, resetting its point value and image 98 | * @param {number} points 99 | */ 100 | showFruit(points) { 101 | this.points = points; 102 | this.animationTarget.style.backgroundImage = this.determineImage( 103 | this.type, points, 104 | ); 105 | this.animationTarget.style.visibility = 'visible'; 106 | } 107 | 108 | /** 109 | * Makes the fruit invisible (happens if Pacman was too slow) 110 | */ 111 | hideFruit() { 112 | this.animationTarget.style.visibility = 'hidden'; 113 | } 114 | 115 | /** 116 | * Returns true if the Pickup is touching a bounding box at Pacman's center 117 | * @param {({ x: number, y: number, size: number})} pickup 118 | * @param {({ x: number, y: number, size: number})} originalPacman 119 | */ 120 | checkForCollision(pickup, originalPacman) { 121 | const pacman = Object.assign({}, originalPacman); 122 | 123 | pacman.x += (pacman.size * 0.25); 124 | pacman.y += (pacman.size * 0.25); 125 | pacman.size /= 2; 126 | 127 | return (pickup.x < pacman.x + pacman.size 128 | && pickup.x + pickup.size > pacman.x 129 | && pickup.y < pacman.y + pacman.size 130 | && pickup.y + pickup.size > pacman.y); 131 | } 132 | 133 | /** 134 | * Checks to see if the pickup is close enough to Pacman to be considered for collision detection 135 | * @param {number} maxDistance - The maximum distance Pacman can travel per cycle 136 | * @param {({ x:number, y:number })} pacmanCenter - The center of Pacman's hitbox 137 | * @param {Boolean} debugging - Flag to change the appearance of pickups for testing 138 | */ 139 | checkPacmanProximity(maxDistance, pacmanCenter, debugging) { 140 | if (this.animationTarget.style.visibility !== 'hidden') { 141 | const distance = Math.sqrt( 142 | ((this.center.x - pacmanCenter.x) ** 2) 143 | + ((this.center.y - pacmanCenter.y) ** 2), 144 | ); 145 | 146 | this.nearPacman = (distance <= maxDistance); 147 | 148 | if (debugging) { 149 | this.animationTarget.style.background = this.nearPacman 150 | ? 'lime' : 'red'; 151 | } 152 | } 153 | } 154 | 155 | /** 156 | * Checks if the pickup is visible and close to Pacman 157 | * @returns {Boolean} 158 | */ 159 | shouldCheckForCollision() { 160 | return this.animationTarget.style.visibility !== 'hidden' 161 | && this.nearPacman; 162 | } 163 | 164 | /** 165 | * If the Pickup is still visible, it checks to see if it is colliding with Pacman. 166 | * It will turn itself invisible and cease collision-detection after the first 167 | * collision with Pacman. 168 | */ 169 | update() { 170 | if (this.shouldCheckForCollision()) { 171 | if (this.checkForCollision( 172 | { 173 | x: this.x, 174 | y: this.y, 175 | size: this.size, 176 | }, { 177 | x: this.pacman.position.left, 178 | y: this.pacman.position.top, 179 | size: this.pacman.measurement, 180 | }, 181 | )) { 182 | this.animationTarget.style.visibility = 'hidden'; 183 | window.dispatchEvent(new CustomEvent('awardPoints', { 184 | detail: { 185 | points: this.points, 186 | type: this.type, 187 | }, 188 | })); 189 | 190 | if (this.type === 'pacdot') { 191 | window.dispatchEvent(new Event('dotEaten')); 192 | } else if (this.type === 'powerPellet') { 193 | window.dispatchEvent(new Event('dotEaten')); 194 | window.dispatchEvent(new Event('powerUp')); 195 | } 196 | } 197 | } 198 | } 199 | } 200 | 201 | // removeIf(production) 202 | module.exports = Pickup; 203 | // endRemoveIf(production) 204 | -------------------------------------------------------------------------------- /app/scripts/utilities/characterUtil.js: -------------------------------------------------------------------------------- 1 | class CharacterUtil { 2 | constructor(scaledTileSize) { 3 | this.scaledTileSize = scaledTileSize; 4 | this.threshold = 5 * this.scaledTileSize; 5 | this.directions = { 6 | up: 'up', 7 | down: 'down', 8 | left: 'left', 9 | right: 'right', 10 | }; 11 | } 12 | 13 | /** 14 | * Check if a given character has moved more than five in-game tiles during a frame. 15 | * If so, we want to temporarily hide the object to avoid 'animation stutter'. 16 | * @param {({top: number, left: number})} position - Position during the current frame 17 | * @param {({top: number, left: number})} oldPosition - Position during the previous frame 18 | * @returns {('hidden'|'visible')} - The new 'visibility' css property value for the character. 19 | */ 20 | checkForStutter(position, oldPosition) { 21 | let stutter = false; 22 | 23 | if (position && oldPosition) { 24 | if (Math.abs(position.top - oldPosition.top) > this.threshold 25 | || Math.abs(position.left - oldPosition.left) > this.threshold) { 26 | stutter = true; 27 | } 28 | } 29 | 30 | return stutter ? 'hidden' : 'visible'; 31 | } 32 | 33 | /** 34 | * Check which CSS property needs to be changed given the character's current direction 35 | * @param {('up'|'down'|'left'|'right')} direction - The character's current travel orientation 36 | * @returns {('top'|'left')} 37 | */ 38 | getPropertyToChange(direction) { 39 | switch (direction) { 40 | case this.directions.up: 41 | case this.directions.down: 42 | return 'top'; 43 | default: 44 | return 'left'; 45 | } 46 | } 47 | 48 | /** 49 | * Calculate the velocity for the character's next frame. 50 | * @param {('up'|'down'|'left'|'right')} direction - The character's current travel orientation 51 | * @param {number} velocityPerMs - The distance to travel in a single millisecond 52 | * @returns {number} - Moving down or right is positive, while up or left is negative. 53 | */ 54 | getVelocity(direction, velocityPerMs) { 55 | switch (direction) { 56 | case this.directions.up: 57 | case this.directions.left: 58 | return velocityPerMs * -1; 59 | default: 60 | return velocityPerMs; 61 | } 62 | } 63 | 64 | /** 65 | * Determine the next value which will be used to draw the character's position on screen 66 | * @param {number} interp - The percentage of the desired timestamp between frames 67 | * @param {('top'|'left')} prop - The css property to be changed 68 | * @param {({top: number, left: number})} oldPosition - Position during the previous frame 69 | * @param {({top: number, left: number})} position - Position during the current frame 70 | * @returns {number} - New value for css positioning 71 | */ 72 | calculateNewDrawValue(interp, prop, oldPosition, position) { 73 | return oldPosition[prop] + (position[prop] - oldPosition[prop]) * interp; 74 | } 75 | 76 | /** 77 | * Convert the character's css position to a row-column on the maze array 78 | * @param {('up'|'down'|'left'|'right')} direction - The character's current travel orientation 79 | * @param {number} scaledTileSize - The dimensions of a single tile 80 | * @returns {({x: number, y: number})} 81 | */ 82 | determineGridPosition(position, scaledTileSize) { 83 | return { 84 | x: (position.left / scaledTileSize) + 0.5, 85 | y: (position.top / scaledTileSize) + 0.5, 86 | }; 87 | } 88 | 89 | /** 90 | * Check to see if a character's desired direction results in turning around 91 | * @param {('up'|'down'|'left'|'right')} direction - The character's current travel orientation 92 | * @param {('up'|'down'|'left'|'right')} desiredDirection - Character's desired orientation 93 | * @returns {boolean} 94 | */ 95 | turningAround(direction, desiredDirection) { 96 | return desiredDirection === this.getOppositeDirection(direction); 97 | } 98 | 99 | /** 100 | * Calculate the opposite of a given direction 101 | * @param {('up'|'down'|'left'|'right')} direction - The character's current travel orientation 102 | * @returns {('up'|'down'|'left'|'right')} 103 | */ 104 | getOppositeDirection(direction) { 105 | switch (direction) { 106 | case this.directions.up: 107 | return this.directions.down; 108 | case this.directions.down: 109 | return this.directions.up; 110 | case this.directions.left: 111 | return this.directions.right; 112 | default: 113 | return this.directions.left; 114 | } 115 | } 116 | 117 | /** 118 | * Calculate the proper rounding function to assist with collision detection 119 | * @param {('up'|'down'|'left'|'right')} direction - The character's current travel orientation 120 | * @returns {Function} 121 | */ 122 | determineRoundingFunction(direction) { 123 | switch (direction) { 124 | case this.directions.up: 125 | case this.directions.left: 126 | return Math.floor; 127 | default: 128 | return Math.ceil; 129 | } 130 | } 131 | 132 | /** 133 | * Check to see if the character's next frame results in moving to a new tile on the maze array 134 | * @param {({x: number, y: number})} oldPosition - Position during the previous frame 135 | * @param {({x: number, y: number})} position - Position during the current frame 136 | * @returns {boolean} 137 | */ 138 | changingGridPosition(oldPosition, position) { 139 | return ( 140 | Math.floor(oldPosition.x) !== Math.floor(position.x) 141 | || Math.floor(oldPosition.y) !== Math.floor(position.y) 142 | ); 143 | } 144 | 145 | /** 146 | * Check to see if the character is attempting to run into a wall of the maze 147 | * @param {({x: number, y: number})} desiredNewGridPosition - Character's target tile 148 | * @param {Array} mazeArray - The 2D array representing the game's maze 149 | * @param {('up'|'down'|'left'|'right')} direction - The character's current travel orientation 150 | * @returns {boolean} 151 | */ 152 | checkForWallCollision(desiredNewGridPosition, mazeArray, direction) { 153 | const roundingFunction = this.determineRoundingFunction( 154 | direction, this.directions, 155 | ); 156 | 157 | const desiredX = roundingFunction(desiredNewGridPosition.x); 158 | const desiredY = roundingFunction(desiredNewGridPosition.y); 159 | let newGridValue; 160 | 161 | if (Array.isArray(mazeArray[desiredY])) { 162 | newGridValue = mazeArray[desiredY][desiredX]; 163 | } 164 | 165 | return (newGridValue === 'X'); 166 | } 167 | 168 | /** 169 | * Returns an object containing the new position and grid position based upon a direction 170 | * @param {({top: number, left: number})} position - css position during the current frame 171 | * @param {('up'|'down'|'left'|'right')} direction - The character's current travel orientation 172 | * @param {number} velocityPerMs - The distance to travel in a single millisecond 173 | * @param {number} elapsedMs - The amount of MS that have passed since the last update 174 | * @param {number} scaledTileSize - The dimensions of a single tile 175 | * @returns {object} 176 | */ 177 | determineNewPositions( 178 | position, direction, velocityPerMs, elapsedMs, scaledTileSize, 179 | ) { 180 | const newPosition = Object.assign({}, position); 181 | newPosition[this.getPropertyToChange(direction)] 182 | += this.getVelocity(direction, velocityPerMs) * elapsedMs; 183 | const newGridPosition = this.determineGridPosition( 184 | newPosition, scaledTileSize, 185 | ); 186 | 187 | return { 188 | newPosition, 189 | newGridPosition, 190 | }; 191 | } 192 | 193 | /** 194 | * Calculates the css position when snapping the character to the x-y grid 195 | * @param {({x: number, y: number})} position - The character's position during the current frame 196 | * @param {('up'|'down'|'left'|'right')} direction - The character's current travel orientation 197 | * @param {number} scaledTileSize - The dimensions of a single tile 198 | * @returns {({top: number, left: number})} 199 | */ 200 | snapToGrid(position, direction, scaledTileSize) { 201 | const newPosition = Object.assign({}, position); 202 | const roundingFunction = this.determineRoundingFunction( 203 | direction, this.directions, 204 | ); 205 | 206 | switch (direction) { 207 | case this.directions.up: 208 | case this.directions.down: 209 | newPosition.y = roundingFunction(newPosition.y); 210 | break; 211 | default: 212 | newPosition.x = roundingFunction(newPosition.x); 213 | break; 214 | } 215 | 216 | return { 217 | top: (newPosition.y - 0.5) * scaledTileSize, 218 | left: (newPosition.x - 0.5) * scaledTileSize, 219 | }; 220 | } 221 | 222 | /** 223 | * Returns a modified position if the character needs to warp 224 | * @param {({top: number, left: number})} position - css position during the current frame 225 | * @param {({x: number, y: number})} gridPosition - x-y position during the current frame 226 | * @param {number} scaledTileSize - The dimensions of a single tile 227 | * @returns {({top: number, left: number})} 228 | */ 229 | handleWarp(position, scaledTileSize, mazeArray) { 230 | const newPosition = Object.assign({}, position); 231 | const gridPosition = this.determineGridPosition(position, scaledTileSize); 232 | 233 | if (gridPosition.x < -0.75) { 234 | newPosition.left = (scaledTileSize * (mazeArray[0].length - 0.75)); 235 | } else if (gridPosition.x > (mazeArray[0].length - 0.25)) { 236 | newPosition.left = (scaledTileSize * -1.25); 237 | } 238 | 239 | return newPosition; 240 | } 241 | 242 | /** 243 | * Advances spritesheet by one frame if needed 244 | * @param {Object} character - The character which needs to be animated 245 | */ 246 | advanceSpriteSheet(character) { 247 | const { 248 | msSinceLastSprite, 249 | animationTarget, 250 | backgroundOffsetPixels, 251 | } = character; 252 | const updatedProperties = { 253 | msSinceLastSprite, 254 | animationTarget, 255 | backgroundOffsetPixels, 256 | }; 257 | 258 | const ready = (character.msSinceLastSprite > character.msBetweenSprites) 259 | && character.animate; 260 | if (ready) { 261 | updatedProperties.msSinceLastSprite = 0; 262 | 263 | if (character.backgroundOffsetPixels 264 | < (character.measurement * (character.spriteFrames - 1)) 265 | ) { 266 | updatedProperties.backgroundOffsetPixels += character.measurement; 267 | } else if (character.loopAnimation) { 268 | updatedProperties.backgroundOffsetPixels = 0; 269 | } 270 | 271 | const style = `-${updatedProperties.backgroundOffsetPixels}px 0px`; 272 | updatedProperties.animationTarget.style.backgroundPosition = style; 273 | } 274 | 275 | return updatedProperties; 276 | } 277 | } 278 | 279 | // removeIf(production) 280 | module.exports = CharacterUtil; 281 | // endRemoveIf(production) 282 | -------------------------------------------------------------------------------- /app/scripts/utilities/soundManager.js: -------------------------------------------------------------------------------- 1 | class SoundManager { 2 | constructor() { 3 | this.baseUrl = 'app/style/audio/'; 4 | this.fileFormat = 'mp3'; 5 | this.masterVolume = 1; 6 | this.paused = false; 7 | this.cutscene = true; 8 | 9 | const AudioContext = window.AudioContext || window.webkitAudioContext; 10 | this.ambience = new AudioContext(); 11 | } 12 | 13 | /** 14 | * Sets the cutscene flag to determine if players should be able to resume ambience 15 | * @param {Boolean} newValue 16 | */ 17 | setCutscene(newValue) { 18 | this.cutscene = newValue; 19 | } 20 | 21 | /** 22 | * Sets the master volume for all sounds and stops/resumes ambience 23 | * @param {(0|1)} newVolume 24 | */ 25 | setMasterVolume(newVolume) { 26 | this.masterVolume = newVolume; 27 | 28 | if (this.soundEffect) { 29 | this.soundEffect.volume = this.masterVolume; 30 | } 31 | 32 | if (this.dotPlayer) { 33 | this.dotPlayer.volume = this.masterVolume; 34 | } 35 | 36 | if (this.masterVolume === 0) { 37 | this.stopAmbience(); 38 | } else { 39 | this.resumeAmbience(this.paused); 40 | } 41 | } 42 | 43 | /** 44 | * Plays a single sound effect 45 | * @param {String} sound 46 | */ 47 | play(sound) { 48 | this.soundEffect = new Audio(`${this.baseUrl}${sound}.${this.fileFormat}`); 49 | this.soundEffect.volume = this.masterVolume; 50 | this.soundEffect.play(); 51 | } 52 | 53 | /** 54 | * Special method for eating dots. The dots should alternate between two 55 | * sound effects, but not too quickly. 56 | */ 57 | playDotSound() { 58 | this.queuedDotSound = true; 59 | 60 | if (!this.dotPlayer) { 61 | this.queuedDotSound = false; 62 | this.dotSound = (this.dotSound === 1) ? 2 : 1; 63 | 64 | this.dotPlayer = new Audio( 65 | `${this.baseUrl}dot_${this.dotSound}.${this.fileFormat}`, 66 | ); 67 | this.dotPlayer.onended = this.dotSoundEnded.bind(this); 68 | this.dotPlayer.volume = this.masterVolume; 69 | this.dotPlayer.play(); 70 | } 71 | } 72 | 73 | /** 74 | * Deletes the dotSound player and plays another dot sound if needed 75 | */ 76 | dotSoundEnded() { 77 | this.dotPlayer = undefined; 78 | 79 | if (this.queuedDotSound) { 80 | this.playDotSound(); 81 | } 82 | } 83 | 84 | /** 85 | * Loops an ambient sound 86 | * @param {String} sound 87 | */ 88 | async setAmbience(sound, keepCurrentAmbience) { 89 | if (!this.fetchingAmbience && !this.cutscene) { 90 | if (!keepCurrentAmbience) { 91 | this.currentAmbience = sound; 92 | this.paused = false; 93 | } else { 94 | this.paused = true; 95 | } 96 | 97 | if (this.ambienceSource) { 98 | this.ambienceSource.stop(); 99 | } 100 | 101 | if (this.masterVolume !== 0) { 102 | this.fetchingAmbience = true; 103 | const response = await fetch( 104 | `${this.baseUrl}${sound}.${this.fileFormat}`, 105 | ); 106 | const arrayBuffer = await response.arrayBuffer(); 107 | const audioBuffer = await this.ambience.decodeAudioData(arrayBuffer); 108 | 109 | this.ambienceSource = this.ambience.createBufferSource(); 110 | this.ambienceSource.buffer = audioBuffer; 111 | this.ambienceSource.connect(this.ambience.destination); 112 | this.ambienceSource.loop = true; 113 | this.ambienceSource.start(); 114 | 115 | this.fetchingAmbience = false; 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * Resumes the ambience 122 | */ 123 | resumeAmbience(paused) { 124 | if (this.ambienceSource) { 125 | // Resetting the ambience since an AudioBufferSourceNode can only 126 | // have 'start()' called once 127 | if (paused) { 128 | this.setAmbience('pause_beat', true); 129 | } else { 130 | this.setAmbience(this.currentAmbience); 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * Stops the ambience 137 | */ 138 | stopAmbience() { 139 | if (this.ambienceSource) { 140 | this.ambienceSource.stop(); 141 | } 142 | } 143 | } 144 | 145 | // removeIf(production) 146 | module.exports = SoundManager; 147 | // endRemoveIf(production) 148 | -------------------------------------------------------------------------------- /app/scripts/utilities/timer.js: -------------------------------------------------------------------------------- 1 | class Timer { 2 | constructor(callback, delay) { 3 | this.callback = callback; 4 | this.remaining = delay; 5 | this.resume(); 6 | } 7 | 8 | /** 9 | * Pauses the timer marks whether the pause came from the player 10 | * or the system 11 | * @param {Boolean} systemPause 12 | */ 13 | pause(systemPause) { 14 | window.clearTimeout(this.timerId); 15 | this.remaining -= new Date() - this.start; 16 | this.oldTimerId = this.timerId; 17 | 18 | if (systemPause) { 19 | this.pausedBySystem = true; 20 | } 21 | } 22 | 23 | /** 24 | * Creates a new setTimeout based upon the remaining time, giving the 25 | * illusion of 'resuming' the old setTimeout 26 | * @param {Boolean} systemResume 27 | */ 28 | resume(systemResume) { 29 | if (systemResume || !this.pausedBySystem) { 30 | this.pausedBySystem = false; 31 | 32 | this.start = new Date(); 33 | this.timerId = window.setTimeout(() => { 34 | this.callback(); 35 | window.dispatchEvent(new CustomEvent('removeTimer', { 36 | detail: { 37 | timer: this, 38 | }, 39 | })); 40 | }, this.remaining); 41 | 42 | if (!this.oldTimerId) { 43 | window.dispatchEvent(new CustomEvent('addTimer', { 44 | detail: { 45 | timer: this, 46 | }, 47 | })); 48 | } 49 | } 50 | } 51 | } 52 | 53 | // removeIf(production) 54 | module.exports = Timer; 55 | // endRemoveIf(production) 56 | -------------------------------------------------------------------------------- /app/style/audio/death.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/audio/death.mp3 -------------------------------------------------------------------------------- /app/style/audio/dot_1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/audio/dot_1.mp3 -------------------------------------------------------------------------------- /app/style/audio/dot_2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/audio/dot_2.mp3 -------------------------------------------------------------------------------- /app/style/audio/eat_ghost.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/audio/eat_ghost.mp3 -------------------------------------------------------------------------------- /app/style/audio/extra_life.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/audio/extra_life.mp3 -------------------------------------------------------------------------------- /app/style/audio/eyes.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/audio/eyes.mp3 -------------------------------------------------------------------------------- /app/style/audio/fruit.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/audio/fruit.mp3 -------------------------------------------------------------------------------- /app/style/audio/game_start.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/audio/game_start.mp3 -------------------------------------------------------------------------------- /app/style/audio/pause.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/audio/pause.mp3 -------------------------------------------------------------------------------- /app/style/audio/pause_beat.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/audio/pause_beat.mp3 -------------------------------------------------------------------------------- /app/style/audio/power_up.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/audio/power_up.mp3 -------------------------------------------------------------------------------- /app/style/audio/siren_1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/audio/siren_1.mp3 -------------------------------------------------------------------------------- /app/style/audio/siren_2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/audio/siren_2.mp3 -------------------------------------------------------------------------------- /app/style/audio/siren_3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/audio/siren_3.mp3 -------------------------------------------------------------------------------- /app/style/graphics/backdrop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/backdrop.png -------------------------------------------------------------------------------- /app/style/graphics/brent_ward_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/brent_ward_logo.png -------------------------------------------------------------------------------- /app/style/graphics/brent_ward_logo.svg: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /app/style/graphics/brent_ward_logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/brent_ward_logo_black.png -------------------------------------------------------------------------------- /app/style/graphics/brent_ward_logo_black.svg: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /app/style/graphics/extra_life.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/extra_life.png -------------------------------------------------------------------------------- /app/style/graphics/extra_life.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/style/graphics/pacman_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/pacman_logo.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_down.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_down.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_down_angry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_down_angry.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_down_angry.svg: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_down_annoyed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_down_annoyed.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_down_annoyed.svg: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_left.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_left.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_left_angry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_left_angry.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_left_angry.svg: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_left_annoyed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_left_annoyed.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_left_annoyed.svg: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_right.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_right.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_right_angry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_right_angry.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_right_angry.svg: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_right_annoyed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_right_annoyed.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_right_annoyed.svg: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_up.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_up.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_up_angry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_up_angry.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_up_angry.svg: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_up_annoyed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_up_annoyed.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/blinky/blinky_up_annoyed.svg: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/clyde/clyde_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/clyde/clyde_down.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/clyde/clyde_down.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/clyde/clyde_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/clyde/clyde_left.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/clyde/clyde_left.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/clyde/clyde_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/clyde/clyde_right.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/clyde/clyde_right.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/clyde/clyde_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/clyde/clyde_up.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/clyde/clyde_up.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/eyes_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/eyes_down.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/eyes_down.svg: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/eyes_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/eyes_left.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/eyes_left.svg: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/eyes_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/eyes_right.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/eyes_right.svg: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/eyes_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/eyes_up.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/eyes_up.svg: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/inky/inky_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/inky/inky_down.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/inky/inky_down.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/inky/inky_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/inky/inky_left.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/inky/inky_left.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/inky/inky_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/inky/inky_right.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/inky/inky_right.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/inky/inky_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/inky/inky_up.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/inky/inky_up.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/pinky/pinky_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/pinky/pinky_down.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/pinky/pinky_down.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/pinky/pinky_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/pinky/pinky_left.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/pinky/pinky_left.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/pinky/pinky_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/pinky/pinky_right.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/pinky/pinky_right.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/pinky/pinky_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/pinky/pinky_up.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/pinky/pinky_up.svg: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/scared_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/scared_blue.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/scared_blue.svg: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/scared_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/ghosts/scared_white.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/ghosts/scared_white.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/pacman/arrow_down.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/arrow_down.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/arrow_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/pacman/arrow_left.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/arrow_left.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/arrow_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/pacman/arrow_right.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/arrow_right.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/arrow_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/pacman/arrow_up.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/arrow_up.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/pacman_death.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/pacman/pacman_death.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/pacman_death.svg: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/pacman_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/pacman/pacman_down.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/pacman_down.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/pacman_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/pacman/pacman_error.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/pacman_error.svg: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/pacman_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/pacman/pacman_left.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/pacman_left.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/pacman_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/pacman/pacman_right.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/pacman_right.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/pacman_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/characters/pacman/pacman_up.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/characters/pacman/pacman_up.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/maze/maze_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/maze/maze_blue.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/maze/maze_blue.svg: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/maze/maze_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/maze/maze_white.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/maze/maze_white.svg: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/pickups/apple.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/apple.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/bell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/pickups/bell.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/bell.svg: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/cherry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/pickups/cherry.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/cherry.svg: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/galaxian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/pickups/galaxian.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/galaxian.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/pickups/key.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/key.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/melon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/pickups/melon.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/melon.svg: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/pickups/orange.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/orange.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/pacdot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/pickups/pacdot.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/pacdot.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/powerPellet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/pickups/powerPellet.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/powerPellet.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/strawberry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/pickups/strawberry.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/pickups/strawberry.svg: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/references/characterSpriteReference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/references/characterSpriteReference.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/references/inky_target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/references/inky_target.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/references/mazeGridSystemReference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/references/mazeGridSystemReference.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/references/mazeSpriteReference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/references/mazeSpriteReference.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/text/100.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/100.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/1000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/text/1000.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/1000.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/1600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/text/1600.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/1600.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/text/200.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/200.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/2000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/text/2000.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/2000.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/text/300.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/300.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/3000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/text/3000.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/3000.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/text/400.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/400.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/text/500.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/500.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/5000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/text/5000.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/5000.svg: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/700.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/text/700.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/700.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/text/800.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/800.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/game_over.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/text/game_over.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/game_over.svg: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/ready.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/app/style/graphics/spriteSheets/text/ready.png -------------------------------------------------------------------------------- /app/style/graphics/spriteSheets/text/ready.svg: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /app/style/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // Animations 2 | $generic-duration: .5s; 3 | $loading-pacman-duration: 1s; 4 | 5 | // Colors 6 | $black: #000; 7 | $blue: #2121ff; 8 | $logo-black: #231f20; 9 | $logo-red: #ee2a29; 10 | $logo-yellow: #fcc73f; 11 | $transparent-gray: rgba(255, 255, 255, .25); 12 | $white: #fff; 13 | $yellow: #ffdf00; 14 | -------------------------------------------------------------------------------- /app/style/scss/ghosts.scss: -------------------------------------------------------------------------------- 1 | .ghost { 2 | margin: 0; 3 | position: absolute; 4 | z-index: 2; 5 | } 6 | -------------------------------------------------------------------------------- /app/style/scss/mainPage.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | * { 4 | font-family: 'Press Start 2P', sans-serif; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | overflow: hidden; 10 | } 11 | 12 | .overflow-mask { 13 | background-color: $black; 14 | color: $white; 15 | display: flex; 16 | height: 100vh; 17 | justify-content: center; 18 | margin: 0; 19 | overflow: hidden; 20 | position: relative; 21 | touch-action: manipulation; 22 | } 23 | 24 | .backdrop { 25 | position: absolute; 26 | visibility: hidden; 27 | } 28 | 29 | .fps-display { 30 | position: absolute; 31 | right: 10px; 32 | top: 10px; 33 | visibility: hidden; 34 | } 35 | 36 | .preload-div { 37 | align-items: baseline; 38 | bottom: 0; 39 | display: flex; 40 | flex-wrap: wrap-reverse; 41 | position: absolute; 42 | visibility: hidden; 43 | } 44 | 45 | .header-buttons { 46 | padding: 2rem 3rem; 47 | width: calc(100vw - 6rem); 48 | position: absolute; 49 | top: 0; 50 | left: 0; 51 | z-index: 2; 52 | display: flex; 53 | align-items: center; 54 | justify-content: space-between; 55 | gap: 1.5rem; 56 | 57 | a { 58 | font-family: none; 59 | display: inline-block; 60 | } 61 | 62 | img { 63 | height: 2rem; 64 | } 65 | 66 | button { 67 | background: none; 68 | border: 0; 69 | color: $white; 70 | cursor: pointer; 71 | outline: none; 72 | padding: 0; 73 | 74 | .material-icons { 75 | font-size: 40px; 76 | } 77 | } 78 | } 79 | 80 | .paused-text { 81 | align-items: center; 82 | display: flex; 83 | font-size: 50px; 84 | height: 100%; 85 | justify-content: center; 86 | left: 0; 87 | position: absolute; 88 | user-select: none; 89 | visibility: hidden; 90 | width: 100%; 91 | z-index: 1; 92 | } 93 | 94 | .game-ui { 95 | display: flex; 96 | flex-direction: column; 97 | justify-content: center; 98 | height: 100%; 99 | 100 | .one-up { 101 | animation: blink 0.6s infinite; 102 | } 103 | 104 | @keyframes blink { 105 | 0% { 106 | opacity: 0; 107 | } 108 | 49% { 109 | opacity: 0; 110 | } 111 | 50% { 112 | opacity: 1; 113 | } 114 | } 115 | 116 | .row { 117 | display: flex; 118 | } 119 | 120 | .top { 121 | ._25 { 122 | width: 25%; 123 | } 124 | 125 | ._50 { 126 | width: 50%; 127 | } 128 | 129 | .column { 130 | :first-child { 131 | text-align: center; 132 | } 133 | 134 | :not(:first-child) { 135 | margin-right: calc(50% - 3em); 136 | text-align: right; 137 | } 138 | } 139 | } 140 | 141 | .bottom { 142 | justify-content: space-between; 143 | 144 | .extra-lives { 145 | align-items: flex-start; 146 | display: flex; 147 | } 148 | 149 | .fruit-display { 150 | display: flex; 151 | flex-direction: row-reverse; 152 | } 153 | } 154 | } 155 | 156 | .loading-cover { 157 | background: $yellow; 158 | height: 100vh; 159 | position: absolute; 160 | top: 0; 161 | width: 50vw; 162 | z-index: 4; 163 | } 164 | 165 | .left { 166 | left: 0; 167 | transition: left $generic-duration ($generic-duration); 168 | } 169 | 170 | .right { 171 | right: 0; 172 | transition: right $generic-duration ($generic-duration); 173 | } 174 | 175 | .main-menu-container { 176 | align-items: center; 177 | display: flex; 178 | flex-direction: column; 179 | height: 100%; 180 | left: 0; 181 | opacity: 0; 182 | position: absolute; 183 | transition: opacity 0.25s; 184 | visibility: hidden; 185 | width: 100%; 186 | z-index: 5; 187 | } 188 | 189 | .brent-ward-logo { 190 | position: absolute; 191 | top: 2rem; 192 | left: 3rem; 193 | 194 | img { 195 | height: 2rem; 196 | } 197 | } 198 | 199 | .logo { 200 | width: 800px; 201 | max-width: 80vw; 202 | margin-top: 25vh; 203 | } 204 | 205 | .game-start { 206 | background-color: $logo-yellow; 207 | border: 5px solid $logo-black; 208 | border-radius: 10px; 209 | box-shadow: 5px 5px $logo-red; 210 | color: $logo-black; 211 | cursor: pointer; 212 | font-size: 48px; 213 | outline: none; 214 | padding: 16px; 215 | 216 | &:active { 217 | box-shadow: none; 218 | transform: translateX(5px) translateY(5px); 219 | } 220 | 221 | &:disabled { 222 | cursor: default; 223 | } 224 | } 225 | 226 | @media only screen and (max-width: 600px) { 227 | .game-start { 228 | scale: 0.5; 229 | } 230 | } 231 | 232 | .loading-container { 233 | background-color: $black; 234 | border: 5px solid $blue; 235 | border-radius: 10px; 236 | height: 48px; 237 | position: absolute; 238 | top: 50%; 239 | transition: opacity $generic-duration $loading-pacman-duration; 240 | width: 500px; 241 | z-index: 5; 242 | } 243 | 244 | @media only screen and (max-width: 600px) { 245 | .loading-container { 246 | transform: scale(0.5); 247 | } 248 | } 249 | 250 | .loading-pacman { 251 | animation: loading-animation 0.3s steps(4) infinite; 252 | background-color: $black; 253 | background-image: url('../app/style/graphics/spriteSheets/characters/pacman/pacman_right.svg'); 254 | background-size: 192px; 255 | border-radius: 10px; 256 | height: 48px; 257 | position: absolute; 258 | transition: left $loading-pacman-duration; 259 | width: 48px; 260 | z-index: 6; 261 | } 262 | 263 | @keyframes loading-animation { 264 | 100% { 265 | background-position: -192px; 266 | } 267 | } 268 | 269 | .loading-dot-mask { 270 | background-color: $black; 271 | border-radius: 10px; 272 | height: 48px; 273 | left: 0; 274 | position: absolute; 275 | top: 0; 276 | transition: width $loading-pacman-duration; 277 | z-index: -1; 278 | } 279 | 280 | .loading-dot { 281 | background-image: url('../app/style/graphics/spriteSheets/pickups/pacdot.svg'); 282 | height: 6px; 283 | position: absolute; 284 | top: 50%; 285 | transform: translateY(-50%); 286 | width: 6px; 287 | z-index: -2; 288 | } 289 | 290 | ._5 { 291 | left: 5%; 292 | } 293 | ._10 { 294 | left: 10%; 295 | } 296 | ._15 { 297 | left: 15%; 298 | } 299 | ._20 { 300 | left: 20%; 301 | } 302 | ._25 { 303 | left: 25%; 304 | } 305 | ._30 { 306 | left: 30%; 307 | } 308 | ._35 { 309 | left: 35%; 310 | } 311 | ._40 { 312 | left: 40%; 313 | } 314 | ._45 { 315 | left: 45%; 316 | } 317 | ._50 { 318 | left: 50%; 319 | } 320 | ._55 { 321 | left: 55%; 322 | } 323 | ._60 { 324 | left: 60%; 325 | } 326 | ._65 { 327 | left: 65%; 328 | } 329 | ._70 { 330 | left: 70%; 331 | } 332 | ._75 { 333 | left: 75%; 334 | } 335 | ._80 { 336 | left: 80%; 337 | } 338 | ._85 { 339 | left: 85%; 340 | } 341 | ._90 { 342 | left: 90%; 343 | } 344 | ._95 { 345 | left: 95%; 346 | } 347 | 348 | .error-message { 349 | color: $logo-black; 350 | opacity: 0; 351 | position: absolute; 352 | top: 30vh; 353 | transition: opacity $generic-duration; 354 | visibility: hidden; 355 | width: 50vw; 356 | z-index: 4; 357 | 358 | .error-pacman { 359 | animation: error-animation 1.5s steps(12) infinite; 360 | background-image: url('../app/style/graphics/spriteSheets/characters/pacman/pacman_error.svg'); 361 | background-size: 576px; 362 | height: 48px; 363 | margin-left: 10px; 364 | width: 48px; 365 | z-index: 6; 366 | } 367 | 368 | @keyframes error-animation { 369 | 100% { 370 | background-position: -576px; 371 | } 372 | } 373 | 374 | .header { 375 | display: flex; 376 | font-size: 50px; 377 | margin-bottom: 30px; 378 | } 379 | 380 | .body { 381 | font-size: 20px; 382 | line-height: 1.25; 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /app/style/scss/maze.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .maze-cover { 4 | background: $black; 5 | height: 100%; 6 | position: absolute; 7 | visibility: hidden; 8 | width: 100%; 9 | z-index: 3; 10 | } 11 | 12 | .maze { 13 | margin: 0 auto; 14 | position: relative; 15 | } 16 | 17 | .maze-img { 18 | position: absolute; 19 | user-select: none; 20 | width: 100%; 21 | } 22 | 23 | .maze-row { 24 | display: flex; 25 | } 26 | 27 | .dot-container { 28 | height: 100%; 29 | position: absolute; 30 | width: 100%; 31 | } 32 | -------------------------------------------------------------------------------- /app/style/scss/pacman.scss: -------------------------------------------------------------------------------- 1 | .pacman { 2 | margin: 0; 3 | position: absolute; 4 | z-index: 1; 5 | } 6 | -------------------------------------------------------------------------------- /app/style/scss/pickups.scss: -------------------------------------------------------------------------------- 1 | .power-pellet { 2 | animation: blink .3s infinite; 3 | } 4 | 5 | @keyframes blink { 6 | 0% {opacity: 0} 7 | 49% {opacity: 0} 8 | 50% {opacity: 1} 9 | } 10 | -------------------------------------------------------------------------------- /app/tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // TODO: Remove this and fix the errors 2 | module.exports = { 3 | "rules": { 4 | "no-multi-assign": "off" 5 | } 6 | }; -------------------------------------------------------------------------------- /app/tests/characterUtil.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const CharacterUtil = require('../scripts/utilities/characterUtil'); 3 | 4 | let characterUtil; 5 | const oldPosition = { top: 0, left: 0 }; 6 | const position = { top: 10, left: 100 }; 7 | const mazeArray = [ 8 | ['X', 'X', 'X'], 9 | ['X', ' ', ' '], 10 | ['X', ' ', 'X'], 11 | ]; 12 | const scaledTileSize = 8; 13 | 14 | beforeEach(() => { 15 | characterUtil = new CharacterUtil(scaledTileSize); 16 | }); 17 | 18 | describe('characterUtil', () => { 19 | describe('checkForStutter', () => { 20 | it('returns VISIBLE if the character moves less than five tiles', () => { 21 | assert.strictEqual(characterUtil.checkForStutter( 22 | oldPosition, { top: 0, left: 0 }, 23 | ), 'visible'); 24 | assert.strictEqual(characterUtil.checkForStutter( 25 | oldPosition, { top: 0, left: 5 }, 26 | ), 'visible'); 27 | assert.strictEqual(characterUtil.checkForStutter( 28 | oldPosition, { top: 5, left: 0 }, 29 | ), 'visible'); 30 | assert.strictEqual(characterUtil.checkForStutter( 31 | oldPosition, { top: 5, left: 5 }, 32 | ), 'visible'); 33 | }); 34 | 35 | it('returns HIDDEN if the character moves more than five tiles', () => { 36 | assert.strictEqual(characterUtil.checkForStutter( 37 | oldPosition, { top: 0, left: 6 * scaledTileSize }, 38 | ), 'hidden'); 39 | assert.strictEqual(characterUtil.checkForStutter( 40 | oldPosition, { top: 0, left: -6 * scaledTileSize }, 41 | ), 'hidden'); 42 | assert.strictEqual(characterUtil.checkForStutter( 43 | oldPosition, { top: 6 * scaledTileSize, left: 0 }, 44 | ), 'hidden'); 45 | assert.strictEqual(characterUtil.checkForStutter( 46 | oldPosition, { top: -6 * scaledTileSize, left: 0 }, 47 | ), 'hidden'); 48 | }); 49 | 50 | it('returns VISIBLE by default if either param is missing', () => { 51 | assert.strictEqual(characterUtil.checkForStutter(), 'visible'); 52 | }); 53 | }); 54 | 55 | describe('getPropertyToChange', () => { 56 | it('returns TOP if the character is moving UP or DOWN', () => { 57 | assert.strictEqual(characterUtil.getPropertyToChange('up'), 'top'); 58 | assert.strictEqual(characterUtil.getPropertyToChange('down'), 'top'); 59 | }); 60 | 61 | it('returns LEFT if the character is moving LEFT or RIGHT', () => { 62 | assert.strictEqual(characterUtil.getPropertyToChange('left'), 'left'); 63 | assert.strictEqual(characterUtil.getPropertyToChange('right'), 'left'); 64 | }); 65 | 66 | it('returns LEFT by default', () => { 67 | assert.strictEqual(characterUtil.getPropertyToChange(), 'left'); 68 | }); 69 | }); 70 | 71 | describe('getVelocity', () => { 72 | it('returns a positive number for DOWN or RIGHT', () => { 73 | assert.strictEqual(characterUtil.getVelocity('down', 100), 100); 74 | assert.strictEqual(characterUtil.getVelocity('right', 100), 100); 75 | }); 76 | 77 | it('returns a negative number for UP or LEFT', () => { 78 | assert.strictEqual(characterUtil.getVelocity('up', 100), -100); 79 | assert.strictEqual(characterUtil.getVelocity('left', 100), -100); 80 | }); 81 | }); 82 | 83 | describe('calculateNewDrawValue', () => { 84 | it('calculates a new value given all parameters', () => { 85 | assert.strictEqual(characterUtil.calculateNewDrawValue( 86 | 1, 'top', oldPosition, position, 87 | ), 10); 88 | assert.strictEqual(characterUtil.calculateNewDrawValue( 89 | 1, 'left', oldPosition, position, 90 | ), 100); 91 | }); 92 | 93 | it('factors in interp when calculating the new value', () => { 94 | assert.strictEqual(characterUtil.calculateNewDrawValue( 95 | 0.5, 'top', oldPosition, position, 96 | ), 5); 97 | assert.strictEqual(characterUtil.calculateNewDrawValue( 98 | 0.5, 'left', oldPosition, position, 99 | ), 50); 100 | }); 101 | }); 102 | 103 | describe('determineGridPosition', () => { 104 | it('returns an x-y object given a valid position', () => { 105 | assert.deepEqual(characterUtil.determineGridPosition( 106 | oldPosition, scaledTileSize, 107 | ), { x: 0.5, y: 0.5 }); 108 | assert.deepEqual(characterUtil.determineGridPosition( 109 | position, scaledTileSize, 110 | ), { x: 13, y: 1.75 }); 111 | }); 112 | }); 113 | 114 | describe('turningAround', () => { 115 | it('returns TRUE if direction and desired direction are opposites', () => { 116 | assert(characterUtil.turningAround('up', 'down')); 117 | assert(characterUtil.turningAround('down', 'up')); 118 | assert(characterUtil.turningAround('left', 'right')); 119 | assert(characterUtil.turningAround('right', 'left')); 120 | }); 121 | 122 | it('returns FALSE if continuing straight or turning to the side', () => { 123 | assert(!characterUtil.turningAround('up', 'up')); 124 | assert(!characterUtil.turningAround('up', 'left')); 125 | assert(!characterUtil.turningAround('up', 'right')); 126 | }); 127 | }); 128 | 129 | describe('getOppositeDirection', () => { 130 | it('returns the opposite of any given direction', () => { 131 | assert.strictEqual(characterUtil.getOppositeDirection('up'), 'down'); 132 | assert.strictEqual(characterUtil.getOppositeDirection('down'), 'up'); 133 | assert.strictEqual(characterUtil.getOppositeDirection('left'), 'right'); 134 | assert.strictEqual(characterUtil.getOppositeDirection('right'), 'left'); 135 | }); 136 | }); 137 | 138 | describe('determineRoundingFunction', () => { 139 | it('returns MATH.FLOOR for UP or LEFT', () => { 140 | assert.strictEqual( 141 | characterUtil.determineRoundingFunction('up'), Math.floor, 142 | ); 143 | assert.strictEqual( 144 | characterUtil.determineRoundingFunction('left'), Math.floor, 145 | ); 146 | }); 147 | 148 | it('returns MATH.CEIL for DOWN or RIGHT', () => { 149 | assert.strictEqual( 150 | characterUtil.determineRoundingFunction('down'), Math.ceil, 151 | ); 152 | assert.strictEqual( 153 | characterUtil.determineRoundingFunction('right'), Math.ceil, 154 | ); 155 | }); 156 | }); 157 | 158 | describe('changingGridPosition', () => { 159 | it('returns TRUE if changing grid positions', () => { 160 | assert(characterUtil.changingGridPosition( 161 | { x: 0, y: 0 }, { x: 0, y: 1 }, 162 | )); 163 | assert(characterUtil.changingGridPosition( 164 | { x: 0, y: 0 }, { x: 1, y: 0 }, 165 | )); 166 | assert(characterUtil.changingGridPosition( 167 | { x: 0, y: 0 }, { x: 1, y: 1 }, 168 | )); 169 | }); 170 | 171 | it('returns FALSE if not', () => { 172 | assert(!characterUtil.changingGridPosition( 173 | { x: 0, y: 0 }, { x: 0, y: 0 }, 174 | )); 175 | assert(!characterUtil.changingGridPosition( 176 | { x: 0, y: 0 }, { x: 0.1, y: 0.9 }, 177 | )); 178 | }); 179 | }); 180 | 181 | describe('checkForWallCollision', () => { 182 | it('returns TRUE if running into a wall', () => { 183 | assert(characterUtil.checkForWallCollision( 184 | { x: 0, y: 1 }, mazeArray, 'left', 185 | )); 186 | assert(characterUtil.checkForWallCollision( 187 | { x: 1, y: 0 }, mazeArray, 'up', 188 | )); 189 | }); 190 | 191 | it('returns FALSE if running to a free tile', () => { 192 | assert(!characterUtil.checkForWallCollision( 193 | { x: 2, y: 1 }, mazeArray, 'right', 194 | )); 195 | assert(!characterUtil.checkForWallCollision( 196 | { x: 1, y: 2 }, mazeArray, 'down', 197 | )); 198 | assert(!characterUtil.checkForWallCollision( 199 | { x: 1, y: 1 }, mazeArray, 'left', 200 | )); 201 | assert(!characterUtil.checkForWallCollision( 202 | { x: 1, y: 1 }, mazeArray, 'up', 203 | )); 204 | }); 205 | 206 | it('returns FALSE if moving outside the maze', () => { 207 | assert(!characterUtil.checkForWallCollision( 208 | { x: -1, y: -1 }, mazeArray, 'right', 209 | )); 210 | assert(!characterUtil.checkForWallCollision( 211 | { x: Infinity, y: Infinity }, mazeArray, 'right', 212 | )); 213 | }); 214 | }); 215 | 216 | describe('determineNewPositions', () => { 217 | it('returns an object containing a position and gridPosition', () => { 218 | const newPositions = characterUtil.determineNewPositions( 219 | { top: 500, left: 500 }, 'up', 5, 20, scaledTileSize, 220 | ); 221 | assert.deepEqual(newPositions, { 222 | newPosition: { top: 400, left: 500 }, 223 | newGridPosition: { x: 63, y: 50.5 }, 224 | }); 225 | }); 226 | }); 227 | 228 | describe('snapToGrid', () => { 229 | const unsnappedPosition = { x: 1.5, y: 1.5 }; 230 | 231 | it('returns a snapped value when traveling in any direction', () => { 232 | assert.deepEqual(characterUtil.snapToGrid( 233 | unsnappedPosition, 'up', scaledTileSize, 234 | ), { top: 4, left: 8 }); 235 | assert.deepEqual(characterUtil.snapToGrid( 236 | unsnappedPosition, 'down', scaledTileSize, 237 | ), { top: 12, left: 8 }); 238 | assert.deepEqual(characterUtil.snapToGrid( 239 | unsnappedPosition, 'left', scaledTileSize, 240 | ), { top: 8, left: 4 }); 241 | assert.deepEqual(characterUtil.snapToGrid( 242 | unsnappedPosition, 'right', scaledTileSize, 243 | ), { top: 8, left: 12 }); 244 | }); 245 | }); 246 | 247 | describe('handleWarp', () => { 248 | it('warps if leaving the maze', () => { 249 | assert.deepEqual(characterUtil.handleWarp( 250 | { top: 0, left: -100 }, scaledTileSize, mazeArray, 251 | ), { top: 0, left: 18 }); 252 | assert.deepEqual(characterUtil.handleWarp( 253 | { top: 0, left: 100 }, scaledTileSize, mazeArray, 254 | ), { top: 0, left: -10 }); 255 | }); 256 | 257 | it('doesn\'t warp otherwise', () => { 258 | assert.deepEqual(characterUtil.handleWarp( 259 | { top: 0, left: 0 }, scaledTileSize, mazeArray, 260 | ), { top: 0, left: 0 }); 261 | }); 262 | }); 263 | 264 | describe('advanceSpriteSheet', () => { 265 | let character; 266 | 267 | beforeEach(() => { 268 | character = { 269 | animate: true, 270 | loopAnimation: true, 271 | msSinceLastSprite: 15, 272 | msBetweenSprites: 10, 273 | moving: true, 274 | animationTarget: { 275 | style: {}, 276 | }, 277 | backgroundOffsetPixels: 50, 278 | measurement: 25, 279 | spriteFrames: 5, 280 | }; 281 | }); 282 | 283 | it('advances animation by one frame if enough time has passed', () => { 284 | const updatedProperties = characterUtil.advanceSpriteSheet(character); 285 | assert.strictEqual(updatedProperties.msSinceLastSprite, 0); 286 | assert.strictEqual( 287 | updatedProperties.animationTarget.style.backgroundPosition, '-75px 0px', 288 | ); 289 | assert.strictEqual(updatedProperties.backgroundOffsetPixels, 75); 290 | }); 291 | 292 | it('returns to the first frame at the spritesheet\'s end', () => { 293 | character.backgroundOffsetPixels = 250; 294 | 295 | const updatedProperties = characterUtil.advanceSpriteSheet(character); 296 | assert.strictEqual(updatedProperties.msSinceLastSprite, 0); 297 | assert.strictEqual( 298 | updatedProperties.animationTarget.style.backgroundPosition, 299 | '-0px 0px', 300 | ); 301 | assert.strictEqual(updatedProperties.backgroundOffsetPixels, 0); 302 | }); 303 | 304 | it('waits for sufficient time between frames', () => { 305 | character.msSinceLastSprite = 5; 306 | 307 | characterUtil.advanceSpriteSheet(character); 308 | assert.strictEqual(character.msSinceLastSprite, 5); 309 | }); 310 | 311 | it('only animates if the character is moving', () => { 312 | character.moving = false; 313 | 314 | characterUtil.advanceSpriteSheet(character); 315 | assert.strictEqual(character.msSinceLastSprite, 15); 316 | }); 317 | 318 | it('only loops animation if loopAnimation is true', () => { 319 | character.loopAnimation = false; 320 | character.backgroundOffsetPixels = 250; 321 | 322 | const updatedProperties = characterUtil.advanceSpriteSheet(character); 323 | assert.strictEqual(updatedProperties.backgroundOffsetPixels, 250); 324 | }); 325 | }); 326 | }); 327 | -------------------------------------------------------------------------------- /app/tests/gameEngine.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const sinon = require('sinon'); 3 | const GameEngine = require('../scripts/core/gameEngine'); 4 | 5 | let gameEngine; 6 | const maxFps = 120; 7 | 8 | beforeEach(() => { 9 | global.document = { 10 | getElementById: () => ({}), 11 | }; 12 | 13 | global.window = { 14 | addEventListener: () => { }, 15 | }; 16 | 17 | global.requestAnimationFrame = (callback) => { 18 | callback(1000); 19 | }; 20 | 21 | gameEngine = new GameEngine(maxFps); 22 | }); 23 | 24 | describe('gameEngine', () => { 25 | describe('changePausedState', () => { 26 | it('pauses the game if it is running', () => { 27 | const stopSpy = gameEngine.stop = sinon.fake(); 28 | gameEngine.changePausedState(true); 29 | assert(stopSpy.called); 30 | }); 31 | 32 | it('resumes the game if it is paused', () => { 33 | const startSpy = gameEngine.start = sinon.fake(); 34 | gameEngine.changePausedState(false); 35 | assert(startSpy.called); 36 | }); 37 | }); 38 | 39 | describe('updateFpsDisplay', () => { 40 | it('updates the FPS display if more than one second has passed', () => { 41 | gameEngine.framesThisSecond = maxFps; 42 | 43 | gameEngine.updateFpsDisplay(1001); 44 | assert.strictEqual(gameEngine.fps, maxFps); 45 | assert.strictEqual(gameEngine.lastFpsUpdate, 1001); 46 | assert.strictEqual(gameEngine.framesThisSecond, 1); 47 | assert.strictEqual(gameEngine.fpsDisplay.textContent, '120 FPS'); 48 | }); 49 | 50 | it('calculates the FPS display as an average', () => { 51 | gameEngine.framesThisSecond = 60; 52 | 53 | gameEngine.updateFpsDisplay(1001); 54 | assert.strictEqual(gameEngine.fps, 90); 55 | assert.strictEqual(gameEngine.lastFpsUpdate, 1001); 56 | assert.strictEqual(gameEngine.framesThisSecond, 1); 57 | assert.strictEqual(gameEngine.fpsDisplay.textContent, '90 FPS'); 58 | }); 59 | 60 | it('doesn\'t update the FPS until a second has passed', () => { 61 | gameEngine.framesThisSecond = 60; 62 | 63 | gameEngine.updateFpsDisplay(1000); 64 | assert.strictEqual(gameEngine.fps, 120); 65 | assert.strictEqual(gameEngine.lastFpsUpdate, 0); 66 | assert.strictEqual(gameEngine.framesThisSecond, 61); 67 | assert.strictEqual(gameEngine.fpsDisplay.textContent, '120 FPS'); 68 | }); 69 | }); 70 | 71 | describe('draw', () => { 72 | it('calls the DRAW function for each entity', () => { 73 | const drawSpy1 = sinon.fake(); 74 | const drawSpy2 = sinon.fake(); 75 | const entityList = [ 76 | { draw: drawSpy1 }, 77 | { draw: drawSpy2 }, 78 | ]; 79 | 80 | gameEngine.draw(50, entityList); 81 | assert(drawSpy1.calledWith(50)); 82 | assert(drawSpy2.calledWith(50)); 83 | }); 84 | 85 | it('won\'t crash if the DRAW property is not a function', () => { 86 | const entityList = [ 87 | { draw: 123 }, 88 | {}, 89 | ]; 90 | 91 | gameEngine.draw(50, entityList); 92 | }); 93 | }); 94 | 95 | describe('update', () => { 96 | it('calls the UPDATE function for each entity', () => { 97 | const updateSpy1 = sinon.fake(); 98 | const updateSpy2 = sinon.fake(); 99 | const entityList = [ 100 | { update: updateSpy1 }, 101 | { update: updateSpy2 }, 102 | ]; 103 | 104 | gameEngine.update(100, entityList); 105 | assert(updateSpy1.calledWith(100)); 106 | assert(updateSpy2.calledWith(100)); 107 | }); 108 | 109 | it('won\'t crash if the UPDATE property is not a function', () => { 110 | const entityList = [ 111 | { update: 123 }, 112 | {}, 113 | ]; 114 | 115 | gameEngine.update(50, entityList); 116 | }); 117 | }); 118 | 119 | describe('panic', () => { 120 | it('resets the elapsedMs value to zero', () => { 121 | gameEngine.elapsedMs = 100; 122 | gameEngine.panic(); 123 | assert.strictEqual(gameEngine.elapsedMs, 0); 124 | }); 125 | }); 126 | 127 | describe('start', () => { 128 | it('calls the mainLoop function to start the engine', () => { 129 | const mainLoopSpy = gameEngine.mainLoop = sinon.fake(); 130 | const drawSpy = gameEngine.draw = sinon.fake(); 131 | 132 | gameEngine.started = false; 133 | gameEngine.start(); 134 | assert(gameEngine.started); 135 | assert(drawSpy.called); 136 | assert(gameEngine.running); 137 | assert.strictEqual(gameEngine.lastFrameTimeMs, 1000); 138 | assert.strictEqual(gameEngine.lastFpsUpdate, 1000); 139 | assert.strictEqual(gameEngine.framesThisSecond, 0); 140 | assert(mainLoopSpy.called); 141 | }); 142 | 143 | it('doesn\'t call the mainLoop again once the engine starts', () => { 144 | const mainLoopSpy = gameEngine.mainLoop = sinon.fake(); 145 | 146 | gameEngine.started = true; 147 | gameEngine.start(); 148 | assert(mainLoopSpy.notCalled); 149 | }); 150 | }); 151 | 152 | describe('stop', () => { 153 | it('stops the engine and cancels the current animation frame', () => { 154 | const cancelSpy = global.cancelAnimationFrame = sinon.fake(); 155 | 156 | gameEngine.running = true; 157 | gameEngine.started = true; 158 | gameEngine.stop(); 159 | assert(!gameEngine.running); 160 | assert(!gameEngine.started); 161 | assert(cancelSpy.called); 162 | }); 163 | }); 164 | 165 | describe('processFrames', () => { 166 | it('calls the update function once per queued timestep', () => { 167 | const updateSpy = gameEngine.update = sinon.fake(); 168 | 169 | gameEngine.elapsedMs = 3; 170 | gameEngine.timestep = 1; 171 | gameEngine.processFrames(); 172 | assert(updateSpy.calledThrice); 173 | }); 174 | 175 | it('calls the panic function if blocked for over a second', () => { 176 | const updateSpy = gameEngine.update = sinon.fake(); 177 | const panicSpy = gameEngine.panic = sinon.fake(); 178 | 179 | gameEngine.elapsedMs = (gameEngine.maxFps * gameEngine.timestep) + 1; 180 | gameEngine.processFrames(); 181 | assert.strictEqual(updateSpy.callCount, gameEngine.maxFps); 182 | assert(panicSpy.called); 183 | }); 184 | }); 185 | 186 | describe('engineCycle', () => { 187 | it('won\'t call functions until a timestep passes', () => { 188 | const mainLoopSpy = gameEngine.mainLoop = sinon.fake(); 189 | const updateFpsDisplaySpy = gameEngine.updateFpsDisplay = sinon.fake(); 190 | const processFramesSpy = gameEngine.processFrames = sinon.fake(); 191 | const drawSpy = gameEngine.draw = sinon.fake(); 192 | gameEngine.elapsedMs = 10000; 193 | gameEngine.lastFrameTimeMs = 9000; 194 | 195 | gameEngine.engineCycle(0); 196 | assert(mainLoopSpy.called); 197 | assert(!updateFpsDisplaySpy.called); 198 | assert(!processFramesSpy.called); 199 | assert(!drawSpy.called); 200 | assert.strictEqual(gameEngine.elapsedMs, 10000); 201 | assert.strictEqual(gameEngine.lastFrameTimeMs, 9000); 202 | }); 203 | 204 | it('calls functions after a timestep passes', () => { 205 | const mainLoopSpy = gameEngine.mainLoop = sinon.fake(); 206 | const updateFpsDisplaySpy = gameEngine.updateFpsDisplay = sinon.fake(); 207 | const processFramesSpy = gameEngine.processFrames = sinon.fake(); 208 | const drawSpy = gameEngine.draw = sinon.fake(); 209 | gameEngine.elapsedMs = 10000; 210 | gameEngine.lastFrameTimeMs = 9000; 211 | 212 | gameEngine.engineCycle(11000); 213 | assert(mainLoopSpy.called); 214 | assert(updateFpsDisplaySpy.called); 215 | assert(processFramesSpy.called); 216 | assert(drawSpy.called); 217 | assert.strictEqual(gameEngine.elapsedMs, 12000); 218 | assert.strictEqual(gameEngine.lastFrameTimeMs, 11000); 219 | }); 220 | }); 221 | 222 | describe('mainLoop', () => { 223 | it('calls the engineCycle function with a timestamp', () => { 224 | const engineCycleSpy = gameEngine.engineCycle = sinon.fake(); 225 | 226 | gameEngine.mainLoop(1000); 227 | assert(engineCycleSpy.calledWith(1000)); 228 | }); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /app/tests/pickup.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const sinon = require('sinon'); 3 | const Pickup = require('../scripts/pickups/pickup'); 4 | 5 | let pickup; 6 | let pacman; 7 | let mazeDiv; 8 | 9 | beforeEach(() => { 10 | global.document = { 11 | createElement: () => ({ 12 | classList: { 13 | add: () => { }, 14 | }, 15 | style: {}, 16 | }), 17 | }; 18 | 19 | pacman = { 20 | position: { 21 | top: 10, 22 | left: 10, 23 | }, 24 | measurement: 16, 25 | }; 26 | 27 | mazeDiv = { 28 | appendChild: () => { }, 29 | }; 30 | 31 | pickup = new Pickup('pacdot', 8, 1, 1, pacman, mazeDiv); 32 | }); 33 | 34 | describe('pickup', () => { 35 | describe('reset', () => { 36 | it('sets visibility according to type', () => { 37 | pickup.animationTarget.style.visibility = 'blah'; 38 | 39 | pickup.type = 'pacdot'; 40 | pickup.reset(); 41 | assert.strictEqual(pickup.animationTarget.style.visibility, 'visible'); 42 | 43 | pickup.type = 'fruit'; 44 | pickup.reset(); 45 | assert.strictEqual(pickup.animationTarget.style.visibility, 'hidden'); 46 | }); 47 | }); 48 | 49 | describe('setStyleMeasurements', () => { 50 | it('sets measurements for pacdots', () => { 51 | pickup.setStyleMeasurements('pacdot', 8, 1, 1); 52 | 53 | assert.strictEqual(pickup.size, 2); 54 | assert.strictEqual(pickup.x, 11); 55 | assert.strictEqual(pickup.y, 11); 56 | assert.deepEqual(pickup.animationTarget.style, { 57 | backgroundImage: 'url(app/style/graphics/spriteSheets/pickups/' 58 | + 'pacdot.svg)', 59 | backgroundSize: '2px', 60 | height: '2px', 61 | left: '11px', 62 | position: 'absolute', 63 | top: '11px', 64 | visibility: 'visible', 65 | width: '2px', 66 | }); 67 | }); 68 | 69 | it('sets measurements for powerPellets', () => { 70 | pickup.setStyleMeasurements('powerPellet', 8, 1, 1); 71 | 72 | assert.strictEqual(pickup.size, 8); 73 | assert.strictEqual(pickup.x, 8); 74 | assert.strictEqual(pickup.y, 8); 75 | assert.deepEqual(pickup.animationTarget.style, { 76 | backgroundImage: 'url(app/style/graphics/spriteSheets/pickups/' 77 | + 'powerPellet.svg)', 78 | backgroundSize: '8px', 79 | height: '8px', 80 | left: '8px', 81 | position: 'absolute', 82 | top: '8px', 83 | visibility: 'visible', 84 | width: '8px', 85 | }); 86 | }); 87 | 88 | it('sets measurements for fruits', () => { 89 | pickup.type = 'fruit'; 90 | pickup.setStyleMeasurements('fruit', 8, 1, 1, 100); 91 | 92 | assert.strictEqual(pickup.size, 16); 93 | assert.strictEqual(pickup.x, 4); 94 | assert.strictEqual(pickup.y, 4); 95 | assert.deepEqual(pickup.animationTarget.style, { 96 | backgroundImage: 'url(app/style/graphics/spriteSheets/pickups/' 97 | + 'cherry.svg)', 98 | backgroundSize: '16px', 99 | height: '16px', 100 | left: '4px', 101 | position: 'absolute', 102 | top: '4px', 103 | width: '16px', 104 | visibility: 'hidden', 105 | }); 106 | }); 107 | }); 108 | 109 | describe('determineImage', () => { 110 | let baseUrl; 111 | 112 | beforeEach(() => { 113 | baseUrl = 'url(app/style/graphics/spriteSheets/pickups/'; 114 | }); 115 | 116 | it('returns correct images for fruits', () => { 117 | assert.strictEqual( 118 | pickup.determineImage('fruit', 100), 119 | `${baseUrl}cherry.svg)`, 120 | ); 121 | 122 | assert.strictEqual( 123 | pickup.determineImage('fruit', 300), 124 | `${baseUrl}strawberry.svg)`, 125 | ); 126 | 127 | assert.strictEqual( 128 | pickup.determineImage('fruit', 500), 129 | `${baseUrl}orange.svg)`, 130 | ); 131 | 132 | assert.strictEqual( 133 | pickup.determineImage('fruit', 700), 134 | `${baseUrl}apple.svg)`, 135 | ); 136 | 137 | assert.strictEqual( 138 | pickup.determineImage('fruit', 1000), 139 | `${baseUrl}melon.svg)`, 140 | ); 141 | 142 | assert.strictEqual( 143 | pickup.determineImage('fruit', 2000), 144 | `${baseUrl}galaxian.svg)`, 145 | ); 146 | 147 | assert.strictEqual( 148 | pickup.determineImage('fruit', 3000), 149 | `${baseUrl}bell.svg)`, 150 | ); 151 | 152 | assert.strictEqual( 153 | pickup.determineImage('fruit', 5000), 154 | `${baseUrl}key.svg)`, 155 | ); 156 | }); 157 | 158 | it('returns cherry by default for unrecognized fruit', () => { 159 | const unknown = pickup.determineImage('fruit', undefined); 160 | assert.strictEqual(unknown, `${baseUrl}cherry.svg)`); 161 | }); 162 | 163 | it('returns correct images for other pickups', () => { 164 | const pacdot = pickup.determineImage('pacdot', undefined); 165 | assert.strictEqual(pacdot, `${baseUrl}pacdot.svg)`); 166 | 167 | const powerPellet = pickup.determineImage('powerPellet', undefined); 168 | assert.strictEqual(powerPellet, `${baseUrl}powerPellet.svg)`); 169 | }); 170 | }); 171 | 172 | describe('showFruit', () => { 173 | it('sets the point value, image, and visibility', () => { 174 | pickup.points = 0; 175 | pickup.animationTarget.style.backgroundImage = ''; 176 | pickup.animationTarget.style.visibility = ''; 177 | pickup.determineImage = sinon.fake.returns('svg'); 178 | 179 | pickup.showFruit(100); 180 | assert.strictEqual(pickup.points, 100); 181 | assert.strictEqual(pickup.animationTarget.style.backgroundImage, 'svg'); 182 | assert.strictEqual(pickup.animationTarget.style.visibility, 'visible'); 183 | }); 184 | }); 185 | 186 | describe('hideFruit', () => { 187 | it('sets the visibility to HIDDEN', () => { 188 | pickup.animationTarget.style.visibility = 'visible'; 189 | 190 | pickup.hideFruit(); 191 | assert.strictEqual(pickup.animationTarget.style.visibility, 'hidden'); 192 | }); 193 | }); 194 | 195 | describe('checkForCollision', () => { 196 | it('returns TRUE if the Pickup is colliding', () => { 197 | assert(pickup.checkForCollision( 198 | { x: 7.4, y: 7.4, size: 5 }, 199 | { x: 0, y: 0, size: 10 }, 200 | )); 201 | }); 202 | 203 | it('returns FALSE if it is not', () => { 204 | assert(!pickup.checkForCollision( 205 | { x: 7.5, y: 7.5, size: 5 }, 206 | { x: 0, y: 0, size: 10 }, 207 | )); 208 | }); 209 | }); 210 | 211 | describe('checkPacmanProximity', () => { 212 | beforeEach(() => { 213 | pickup.center = { x: 0, y: 0 }; 214 | }); 215 | 216 | it('returns TRUE if the pickup is close to Pacman', () => { 217 | pickup.checkPacmanProximity(5, { x: 3, y: 4 }); 218 | assert(pickup.nearPacman); 219 | }); 220 | 221 | it('returns FALSE otherwise', () => { 222 | pickup.checkPacmanProximity(4.9, { x: 3, y: 4 }); 223 | assert(!pickup.nearPacman); 224 | }); 225 | 226 | it('sets background color when debugging', () => { 227 | pickup.checkPacmanProximity(5, { x: 3, y: 4 }, true); 228 | assert.strictEqual(pickup.animationTarget.style.background, 'lime'); 229 | 230 | pickup.checkPacmanProximity(4.9, { x: 3, y: 4 }, true); 231 | assert.strictEqual(pickup.animationTarget.style.background, 'red'); 232 | }); 233 | 234 | it('skips execution if the pickup is hidden', () => { 235 | pickup.animationTarget.style.visibility = 'hidden'; 236 | pickup.nearPacman = false; 237 | 238 | pickup.checkPacmanProximity(5, { x: 3, y: 4 }); 239 | assert(!pickup.nearPacman); 240 | }); 241 | }); 242 | 243 | describe('shouldCheckForCollision', () => { 244 | it('only returns TRUE when the Pickup is near Pacman and visible', () => { 245 | pickup.animationTarget.style.visibility = 'visible'; 246 | pickup.nearPacman = true; 247 | assert(pickup.shouldCheckForCollision()); 248 | 249 | pickup.nearPacman = false; 250 | assert(!pickup.shouldCheckForCollision()); 251 | 252 | pickup.animationTarget.style.visibility = 'hidden'; 253 | assert(!pickup.shouldCheckForCollision()); 254 | 255 | pickup.nearPacman = true; 256 | assert(!pickup.shouldCheckForCollision()); 257 | }); 258 | }); 259 | 260 | describe('update', () => { 261 | beforeEach(() => { 262 | global.window = { 263 | dispatchEvent: sinon.fake(), 264 | }; 265 | }); 266 | 267 | it('turns the Pickup\'s visibility to HIDDEN after collision', () => { 268 | pickup.shouldCheckForCollision = sinon.fake.returns(true); 269 | pickup.checkForCollision = sinon.fake.returns(true); 270 | 271 | pickup.update(); 272 | assert.strictEqual(pickup.animationTarget.style.visibility, 'hidden'); 273 | }); 274 | 275 | it('leaves the Pickup\'s visibility until collision', () => { 276 | pickup.shouldCheckForCollision = sinon.fake.returns(true); 277 | pickup.checkForCollision = sinon.fake.returns(false); 278 | 279 | pickup.update(); 280 | assert.notStrictEqual(pickup.animationTarget.style.visibility, 'hidden'); 281 | }); 282 | 283 | it('emits the awardPoints event after a collision', () => { 284 | pickup.points = 100; 285 | pickup.shouldCheckForCollision = sinon.fake.returns(true); 286 | pickup.checkForCollision = sinon.fake.returns(true); 287 | 288 | pickup.update(); 289 | assert(global.window.dispatchEvent.calledWith( 290 | new CustomEvent('awardPoints', { 291 | detail: { 292 | points: pickup.points, 293 | }, 294 | }), 295 | )); 296 | }); 297 | 298 | it('emits dotEaten event if a pacdot collides with Pacman', () => { 299 | pickup.type = 'pacdot'; 300 | pickup.shouldCheckForCollision = sinon.fake.returns(true); 301 | pickup.checkForCollision = sinon.fake.returns(true); 302 | 303 | pickup.update(); 304 | assert(global.window.dispatchEvent.calledWith(new Event('dotEaten'))); 305 | }); 306 | 307 | it('emits powerUp event if a powerPellet collides with Pacman', () => { 308 | pickup.type = 'powerPellet'; 309 | pickup.shouldCheckForCollision = sinon.fake.returns(true); 310 | pickup.checkForCollision = sinon.fake.returns(true); 311 | 312 | pickup.update(); 313 | assert(global.window.dispatchEvent.calledWith(new Event('dotEaten'))); 314 | assert(global.window.dispatchEvent.calledWith(new Event('powerUp'))); 315 | }); 316 | 317 | it('emits no events if an unrecognized item collides with Pacman', () => { 318 | pickup.type = 'blah'; 319 | pickup.shouldCheckForCollision = sinon.fake.returns(true); 320 | pickup.checkForCollision = sinon.fake.returns(true); 321 | 322 | pickup.update(); 323 | }); 324 | 325 | it('does nothing if shouldCheckForCollision returns FALSE', () => { 326 | pickup.shouldCheckForCollision = sinon.fake.returns(false); 327 | pickup.checkForCollision = sinon.fake(); 328 | 329 | pickup.update(); 330 | assert(!pickup.checkForCollision.called); 331 | }); 332 | }); 333 | }); 334 | -------------------------------------------------------------------------------- /app/tests/soundManager.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const sinon = require('sinon'); 3 | const SoundManager = require('../scripts/utilities/soundManager'); 4 | 5 | let comp; 6 | 7 | describe('soundManager', () => { 8 | beforeEach(() => { 9 | global.Audio = class { 10 | play() { } 11 | }; 12 | const AudioContext = class { }; 13 | global.window = { 14 | AudioContext, 15 | }; 16 | 17 | comp = new SoundManager(); 18 | }); 19 | 20 | describe('constructor', () => { 21 | it('uses webkitAudioContext if needed', () => { 22 | global.window.AudioContext = undefined; 23 | global.window.webkitAudioContext = class { }; 24 | 25 | const testComp = new SoundManager(); 26 | assert.notEqual(testComp.ambience, undefined); 27 | }); 28 | }); 29 | 30 | describe('setCutscene', () => { 31 | it('sets new values for cutscene', () => { 32 | comp.cutscene = false; 33 | 34 | comp.setCutscene(true); 35 | assert.strictEqual(comp.cutscene, true); 36 | }); 37 | }); 38 | 39 | describe('setMasterVolume', () => { 40 | it('sets the master volume for all sounds and toggles ambience', () => { 41 | comp.stopAmbience = sinon.fake(); 42 | comp.resumeAmbience = sinon.fake(); 43 | 44 | comp.setMasterVolume(1); 45 | assert(comp.resumeAmbience.calledWith(comp.paused)); 46 | 47 | comp.soundEffect = {}; 48 | comp.dotPlayer = {}; 49 | comp.setMasterVolume(0); 50 | assert.strictEqual(comp.soundEffect.volume, 0); 51 | assert.strictEqual(comp.dotPlayer.volume, 0); 52 | assert(comp.stopAmbience.called); 53 | }); 54 | }); 55 | 56 | describe('play', () => { 57 | it('plays a given sound effect', () => { 58 | const spy = sinon.spy(global, 'Audio'); 59 | 60 | comp.play('some_sound'); 61 | assert(spy.calledWith('app/style/audio/some_sound.mp3')); 62 | }); 63 | }); 64 | 65 | describe('playDotSound', () => { 66 | it('alternates between two dot sounds', () => { 67 | const spy = sinon.spy(global, 'Audio'); 68 | 69 | comp.playDotSound(); 70 | assert(spy.calledWith('app/style/audio/dot_1.mp3')); 71 | 72 | comp.dotPlayer = undefined; 73 | comp.playDotSound(); 74 | assert(spy.calledWith('app/style/audio/dot_2.mp3')); 75 | }); 76 | 77 | it('does nothing if another dot sound is already playing', () => { 78 | comp.dotPlayer = {}; 79 | const spy = sinon.spy(global, 'Audio'); 80 | 81 | comp.playDotSound(); 82 | assert(!spy.called); 83 | }); 84 | }); 85 | 86 | describe('dotSoundEnded', () => { 87 | it('deletes the current dotPlayer', () => { 88 | comp.dotPlayer = {}; 89 | 90 | comp.dotSoundEnded(); 91 | assert.strictEqual(comp.dotPlayer, undefined); 92 | }); 93 | 94 | it('calls playDotSound if queuedDotSound is TRUE', () => { 95 | comp.queuedDotSound = true; 96 | comp.playDotSound = sinon.fake(); 97 | 98 | comp.dotSoundEnded(); 99 | assert(comp.playDotSound.called); 100 | }); 101 | }); 102 | 103 | describe('setAmbience', () => { 104 | const arraySpy = sinon.fake(); 105 | const connectSpy = sinon.fake(); 106 | const startSpy = sinon.fake(); 107 | 108 | beforeEach(() => { 109 | global.fetch = sinon.fake.returns({ 110 | arrayBuffer: arraySpy, 111 | }); 112 | comp.ambience.decodeAudioData = sinon.fake(); 113 | comp.ambience.createBufferSource = sinon.fake.returns({ 114 | connect: connectSpy, 115 | start: startSpy, 116 | }); 117 | comp.cutscene = false; 118 | }); 119 | 120 | it('does nothing if fetchingAmbience is TRUE', () => { 121 | comp.fetchingAmbience = true; 122 | 123 | comp.setAmbience('some_sound'); 124 | assert(!arraySpy.called); 125 | }); 126 | 127 | it('does not start new ambience if masterVolume is ZERO', () => { 128 | comp.masterVolume = 0; 129 | 130 | comp.setAmbience('some_sound'); 131 | assert(!arraySpy.called); 132 | }); 133 | 134 | it('loops an ambient sound', async () => { 135 | await comp.setAmbience('some_sound'); 136 | assert(global.fetch.calledWith('app/style/audio/some_sound.mp3')); 137 | assert(arraySpy.called); 138 | assert(comp.ambience.decodeAudioData.called); 139 | assert(comp.ambience.createBufferSource.called); 140 | assert(connectSpy.calledWith(comp.ambience.destination)); 141 | assert.strictEqual(comp.ambienceSource.loop, true); 142 | assert(startSpy.called); 143 | }); 144 | 145 | it('stops previously running ambience', () => { 146 | comp.ambienceSource = { 147 | stop: sinon.fake(), 148 | }; 149 | 150 | comp.setAmbience('some_sound'); 151 | assert(comp.ambienceSource.stop.called); 152 | }); 153 | 154 | it('keeps the current ambience if needed', () => { 155 | comp.currentAmbience = 'blah'; 156 | 157 | comp.setAmbience('some_sound', true); 158 | assert.strictEqual(comp.currentAmbience, 'blah'); 159 | }); 160 | }); 161 | 162 | describe('resumeAmbience', () => { 163 | it('resumes an existing ambience', () => { 164 | comp.setAmbience = sinon.fake(); 165 | 166 | comp.resumeAmbience(); 167 | assert(!comp.setAmbience.called); 168 | 169 | comp.ambienceSource = {}; 170 | comp.resumeAmbience(); 171 | assert(comp.setAmbience.calledWith(comp.currentAmbience)); 172 | }); 173 | 174 | it('sets ambience to pause_beat if the game is paused', () => { 175 | comp.setAmbience = sinon.fake(); 176 | comp.ambienceSource = {}; 177 | 178 | comp.resumeAmbience(true); 179 | assert(comp.setAmbience.calledWith('pause_beat')); 180 | }); 181 | }); 182 | 183 | describe('stopAmbience', () => { 184 | it('stops existing ambience', () => { 185 | comp.stopAmbience(); 186 | 187 | comp.ambienceSource = { 188 | stop: sinon.fake(), 189 | }; 190 | comp.stopAmbience(); 191 | assert(comp.ambienceSource.stop.calledOnce); 192 | }); 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /app/tests/timer.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const sinon = require('sinon'); 3 | const Timer = require('../scripts/utilities/timer'); 4 | 5 | let comp; 6 | let clock; 7 | 8 | describe('timer', () => { 9 | beforeEach(() => { 10 | window.setTimeout = () => { }; 11 | window.dispatchEvent = () => { }; 12 | clock = sinon.useFakeTimers(); 13 | 14 | comp = new Timer(undefined, 1000); 15 | }); 16 | 17 | afterEach(() => { 18 | clock.restore(); 19 | }); 20 | 21 | describe('pause', () => { 22 | beforeEach(() => { 23 | window.clearTimeout = sinon.fake(); 24 | }); 25 | 26 | it('clears the timeout and adjusts remaining time', () => { 27 | comp.timerId = 2; 28 | comp.start = new Date(); 29 | clock.tick(500); 30 | 31 | comp.pause(); 32 | assert(window.clearTimeout.calledWith(comp.timerId)); 33 | assert.strictEqual(comp.remaining, 500); 34 | }); 35 | 36 | it('sets a flag when paused by the system', () => { 37 | comp.pause(true); 38 | assert(comp.pausedBySystem); 39 | }); 40 | }); 41 | 42 | describe('resume', () => { 43 | it('executes the callback on a delay and dispatches events', () => { 44 | window.setTimeout = setTimeout; 45 | comp.callback = sinon.fake(); 46 | window.dispatchEvent = sinon.fake(); 47 | 48 | comp.resume(); 49 | assert.strictEqual(comp.pausedBySystem, false); 50 | assert.deepEqual(comp.start, new Date()); 51 | assert(!comp.callback.called); 52 | assert(window.dispatchEvent.calledWith(new CustomEvent('addTimer', { 53 | detail: { 54 | timer: comp, 55 | }, 56 | }))); 57 | 58 | clock.tick(1000); 59 | assert(comp.callback.called); 60 | assert(window.dispatchEvent.calledWith(new CustomEvent('removeTimer', { 61 | detail: { 62 | id: comp.timerId, 63 | }, 64 | }))); 65 | }); 66 | 67 | it('ignores players resuming timers paused by the system', () => { 68 | comp.pausedBySystem = true; 69 | window.setTimeout = sinon.fake(); 70 | 71 | comp.resume(); 72 | assert(!window.setTimeout.called); 73 | }); 74 | 75 | it('only dispatches the addTimer event once', () => { 76 | comp.oldTimerId = 1; 77 | window.dispatchEvent = sinon.fake(); 78 | 79 | comp.resume(); 80 | assert(!window.dispatchEvent.called); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /build/app.css: -------------------------------------------------------------------------------- 1 | .ghost { 2 | margin: 0; 3 | position: absolute; 4 | z-index: 2; 5 | } 6 | * { 7 | font-family: "Press Start 2P", sans-serif; 8 | } 9 | 10 | body { 11 | margin: 0; 12 | overflow: hidden; 13 | } 14 | 15 | .overflow-mask { 16 | background-color: #000; 17 | color: #fff; 18 | display: flex; 19 | height: 100vh; 20 | justify-content: center; 21 | margin: 0; 22 | overflow: hidden; 23 | position: relative; 24 | touch-action: manipulation; 25 | } 26 | 27 | .backdrop { 28 | position: absolute; 29 | visibility: hidden; 30 | } 31 | 32 | .fps-display { 33 | position: absolute; 34 | right: 10px; 35 | top: 10px; 36 | visibility: hidden; 37 | } 38 | 39 | .preload-div { 40 | align-items: baseline; 41 | bottom: 0; 42 | display: flex; 43 | flex-wrap: wrap-reverse; 44 | position: absolute; 45 | visibility: hidden; 46 | } 47 | 48 | .header-buttons { 49 | padding: 2rem 3rem; 50 | width: calc(100vw - 6rem); 51 | position: absolute; 52 | top: 0; 53 | left: 0; 54 | z-index: 2; 55 | display: flex; 56 | align-items: center; 57 | justify-content: space-between; 58 | gap: 1.5rem; 59 | } 60 | .header-buttons a { 61 | font-family: none; 62 | display: inline-block; 63 | } 64 | .header-buttons img { 65 | height: 2rem; 66 | } 67 | .header-buttons button { 68 | background: none; 69 | border: 0; 70 | color: #fff; 71 | cursor: pointer; 72 | outline: none; 73 | padding: 0; 74 | } 75 | .header-buttons button .material-icons { 76 | font-size: 40px; 77 | } 78 | 79 | .paused-text { 80 | align-items: center; 81 | display: flex; 82 | font-size: 50px; 83 | height: 100%; 84 | justify-content: center; 85 | left: 0; 86 | position: absolute; 87 | user-select: none; 88 | visibility: hidden; 89 | width: 100%; 90 | z-index: 1; 91 | } 92 | 93 | .game-ui { 94 | display: flex; 95 | flex-direction: column; 96 | justify-content: center; 97 | height: 100%; 98 | } 99 | .game-ui .one-up { 100 | animation: blink 0.6s infinite; 101 | } 102 | @keyframes blink { 103 | 0% { 104 | opacity: 0; 105 | } 106 | 49% { 107 | opacity: 0; 108 | } 109 | 50% { 110 | opacity: 1; 111 | } 112 | } 113 | .game-ui .row { 114 | display: flex; 115 | } 116 | .game-ui .top ._25 { 117 | width: 25%; 118 | } 119 | .game-ui .top ._50 { 120 | width: 50%; 121 | } 122 | .game-ui .top .column :first-child { 123 | text-align: center; 124 | } 125 | .game-ui .top .column :not(:first-child) { 126 | margin-right: calc(50% - 3em); 127 | text-align: right; 128 | } 129 | .game-ui .bottom { 130 | justify-content: space-between; 131 | } 132 | .game-ui .bottom .extra-lives { 133 | align-items: flex-start; 134 | display: flex; 135 | } 136 | .game-ui .bottom .fruit-display { 137 | display: flex; 138 | flex-direction: row-reverse; 139 | } 140 | 141 | .loading-cover { 142 | background: #ffdf00; 143 | height: 100vh; 144 | position: absolute; 145 | top: 0; 146 | width: 50vw; 147 | z-index: 4; 148 | } 149 | 150 | .left { 151 | left: 0; 152 | transition: left 0.5s 0.5s; 153 | } 154 | 155 | .right { 156 | right: 0; 157 | transition: right 0.5s 0.5s; 158 | } 159 | 160 | .main-menu-container { 161 | align-items: center; 162 | display: flex; 163 | flex-direction: column; 164 | height: 100%; 165 | left: 0; 166 | opacity: 0; 167 | position: absolute; 168 | transition: opacity 0.25s; 169 | visibility: hidden; 170 | width: 100%; 171 | z-index: 5; 172 | } 173 | 174 | .brent-ward-logo { 175 | position: absolute; 176 | top: 2rem; 177 | left: 3rem; 178 | } 179 | .brent-ward-logo img { 180 | height: 2rem; 181 | } 182 | 183 | .logo { 184 | width: 800px; 185 | max-width: 80vw; 186 | margin-top: 25vh; 187 | } 188 | 189 | .game-start { 190 | background-color: #fcc73f; 191 | border: 5px solid #231f20; 192 | border-radius: 10px; 193 | box-shadow: 5px 5px #ee2a29; 194 | color: #231f20; 195 | cursor: pointer; 196 | font-size: 48px; 197 | outline: none; 198 | padding: 16px; 199 | } 200 | .game-start:active { 201 | box-shadow: none; 202 | transform: translateX(5px) translateY(5px); 203 | } 204 | .game-start:disabled { 205 | cursor: default; 206 | } 207 | 208 | @media only screen and (max-width: 600px) { 209 | .game-start { 210 | scale: 0.5; 211 | } 212 | } 213 | .loading-container { 214 | background-color: #000; 215 | border: 5px solid #2121ff; 216 | border-radius: 10px; 217 | height: 48px; 218 | position: absolute; 219 | top: 50%; 220 | transition: opacity 0.5s 1s; 221 | width: 500px; 222 | z-index: 5; 223 | } 224 | 225 | @media only screen and (max-width: 600px) { 226 | .loading-container { 227 | transform: scale(0.5); 228 | } 229 | } 230 | .loading-pacman { 231 | animation: loading-animation 0.3s steps(4) infinite; 232 | background-color: #000; 233 | background-image: url("../app/style/graphics/spriteSheets/characters/pacman/pacman_right.svg"); 234 | background-size: 192px; 235 | border-radius: 10px; 236 | height: 48px; 237 | position: absolute; 238 | transition: left 1s; 239 | width: 48px; 240 | z-index: 6; 241 | } 242 | 243 | @keyframes loading-animation { 244 | 100% { 245 | background-position: -192px; 246 | } 247 | } 248 | .loading-dot-mask { 249 | background-color: #000; 250 | border-radius: 10px; 251 | height: 48px; 252 | left: 0; 253 | position: absolute; 254 | top: 0; 255 | transition: width 1s; 256 | z-index: -1; 257 | } 258 | 259 | .loading-dot { 260 | background-image: url("../app/style/graphics/spriteSheets/pickups/pacdot.svg"); 261 | height: 6px; 262 | position: absolute; 263 | top: 50%; 264 | transform: translateY(-50%); 265 | width: 6px; 266 | z-index: -2; 267 | } 268 | 269 | ._5 { 270 | left: 5%; 271 | } 272 | 273 | ._10 { 274 | left: 10%; 275 | } 276 | 277 | ._15 { 278 | left: 15%; 279 | } 280 | 281 | ._20 { 282 | left: 20%; 283 | } 284 | 285 | ._25 { 286 | left: 25%; 287 | } 288 | 289 | ._30 { 290 | left: 30%; 291 | } 292 | 293 | ._35 { 294 | left: 35%; 295 | } 296 | 297 | ._40 { 298 | left: 40%; 299 | } 300 | 301 | ._45 { 302 | left: 45%; 303 | } 304 | 305 | ._50 { 306 | left: 50%; 307 | } 308 | 309 | ._55 { 310 | left: 55%; 311 | } 312 | 313 | ._60 { 314 | left: 60%; 315 | } 316 | 317 | ._65 { 318 | left: 65%; 319 | } 320 | 321 | ._70 { 322 | left: 70%; 323 | } 324 | 325 | ._75 { 326 | left: 75%; 327 | } 328 | 329 | ._80 { 330 | left: 80%; 331 | } 332 | 333 | ._85 { 334 | left: 85%; 335 | } 336 | 337 | ._90 { 338 | left: 90%; 339 | } 340 | 341 | ._95 { 342 | left: 95%; 343 | } 344 | 345 | .error-message { 346 | color: #231f20; 347 | opacity: 0; 348 | position: absolute; 349 | top: 30vh; 350 | transition: opacity 0.5s; 351 | visibility: hidden; 352 | width: 50vw; 353 | z-index: 4; 354 | } 355 | .error-message .error-pacman { 356 | animation: error-animation 1.5s steps(12) infinite; 357 | background-image: url("../app/style/graphics/spriteSheets/characters/pacman/pacman_error.svg"); 358 | background-size: 576px; 359 | height: 48px; 360 | margin-left: 10px; 361 | width: 48px; 362 | z-index: 6; 363 | } 364 | @keyframes error-animation { 365 | 100% { 366 | background-position: -576px; 367 | } 368 | } 369 | .error-message .header { 370 | display: flex; 371 | font-size: 50px; 372 | margin-bottom: 30px; 373 | } 374 | .error-message .body { 375 | font-size: 20px; 376 | line-height: 1.25; 377 | } 378 | .maze-cover { 379 | background: #000; 380 | height: 100%; 381 | position: absolute; 382 | visibility: hidden; 383 | width: 100%; 384 | z-index: 3; 385 | } 386 | 387 | .maze { 388 | margin: 0 auto; 389 | position: relative; 390 | } 391 | 392 | .maze-img { 393 | position: absolute; 394 | user-select: none; 395 | width: 100%; 396 | } 397 | 398 | .maze-row { 399 | display: flex; 400 | } 401 | 402 | .dot-container { 403 | height: 100%; 404 | position: absolute; 405 | width: 100%; 406 | } 407 | .pacman { 408 | margin: 0; 409 | position: absolute; 410 | z-index: 1; 411 | } 412 | .power-pellet { 413 | animation: blink 0.3s infinite; 414 | } 415 | 416 | @keyframes blink { 417 | 0% { 418 | opacity: 0; 419 | } 420 | 49% { 421 | opacity: 0; 422 | } 423 | 50% { 424 | opacity: 1; 425 | } 426 | } -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bward2/pacman-js/89e7595d4266cf0b38df476bdcb9bb8957ea8afd/favicon.ico -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const sass = require('gulp-sass')(require('sass')); 3 | const concat = require('gulp-concat'); 4 | const removeCode = require('gulp-remove-code'); 5 | 6 | function styles() { 7 | return gulp 8 | .src('app/style/scss/**/*.scss') 9 | .pipe(sass()) 10 | .pipe(concat('app.css')) 11 | .pipe(gulp.dest('build')); 12 | } 13 | 14 | function scripts() { 15 | return gulp 16 | .src('app/scripts/**/*.js') 17 | .pipe(removeCode({ production: true })) 18 | .pipe(concat('app.js')) 19 | .pipe(gulp.dest('build')); 20 | } 21 | 22 | function watch() { 23 | gulp.watch('app/style/**/*.scss', styles); 24 | gulp.watch('app/scripts/**/*.js', scripts); 25 | } 26 | 27 | const build = gulp.parallel(styles, scripts); 28 | 29 | exports.watch = watch; 30 | exports.default = build; 31 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 19 |