├── img ├── orc.png ├── tower.png ├── gameMap.png ├── explosion.png └── projectile.png ├── .prettierrc ├── js ├── classes │ ├── Projectile.js │ ├── PlacementTile.js │ ├── Sprite.js │ ├── Building.js │ └── Enemy.js ├── waypoints.js ├── placementTilesData.js └── index.js └── index.html /img/orc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscourses/tower-defense/HEAD/img/orc.png -------------------------------------------------------------------------------- /img/tower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscourses/tower-defense/HEAD/img/tower.png -------------------------------------------------------------------------------- /img/gameMap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscourses/tower-defense/HEAD/img/gameMap.png -------------------------------------------------------------------------------- /img/explosion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscourses/tower-defense/HEAD/img/explosion.png -------------------------------------------------------------------------------- /img/projectile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscourses/tower-defense/HEAD/img/projectile.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "none", 5 | "overrides": [ 6 | { 7 | "files": "*.html", 8 | "options": { 9 | "parser": "html" 10 | } 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /js/classes/Projectile.js: -------------------------------------------------------------------------------- 1 | class Projectile extends Sprite { 2 | constructor({ position = { x: 0, y: 0 }, enemy }) { 3 | super({ position, imageSrc: 'img/projectile.png' }) 4 | this.velocity = { 5 | x: 0, 6 | y: 0 7 | } 8 | this.enemy = enemy 9 | this.radius = 10 10 | } 11 | 12 | update() { 13 | this.draw() 14 | 15 | const angle = Math.atan2( 16 | this.enemy.center.y - this.position.y, 17 | this.enemy.center.x - this.position.x 18 | ) 19 | 20 | const power = 5 21 | this.velocity.x = Math.cos(angle) * power 22 | this.velocity.y = Math.sin(angle) * power 23 | 24 | this.position.x += this.velocity.x 25 | this.position.y += this.velocity.y 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /js/classes/PlacementTile.js: -------------------------------------------------------------------------------- 1 | class PlacementTile { 2 | constructor({ position = { x: 0, y: 0 } }) { 3 | this.position = position 4 | this.size = 64 5 | this.color = 'rgba(255, 255, 255, 0.15)' 6 | this.occupied = false 7 | } 8 | 9 | draw() { 10 | c.fillStyle = this.color 11 | c.fillRect(this.position.x, this.position.y, this.size, this.size) 12 | } 13 | 14 | update(mouse) { 15 | this.draw() 16 | 17 | if ( 18 | mouse.x > this.position.x && 19 | mouse.x < this.position.x + this.size && 20 | mouse.y > this.position.y && 21 | mouse.y < this.position.y + this.size 22 | ) { 23 | this.color = 'white' 24 | } else this.color = 'rgba(255, 255, 255, 0.15)' 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /js/waypoints.js: -------------------------------------------------------------------------------- 1 | const waypoints = [ 2 | { 3 | x: -124.21331566744, 4 | y: 475.322954620735 5 | }, 6 | { 7 | x: 276.581649552832, 8 | y: 472.01059953627 9 | }, 10 | { 11 | x: 276.581649552832, 12 | y: 157.33686651209 13 | }, 14 | { 15 | x: 735.342828751242, 16 | y: 158.993044054323 17 | }, 18 | { 19 | x: 735.342828751242, 20 | y: 405.763497846969 21 | }, 22 | { 23 | x: 606.160980457105, 24 | y: 407.419675389202 25 | }, 26 | { 27 | x: 606.160980457105, 28 | y: 669.095727061941 29 | }, 30 | { 31 | x: 1046.70420669096, 32 | y: 665.783371977476 33 | }, 34 | { 35 | x: 1050.01656177542, 36 | y: 288.17489234846 37 | }, 38 | { 39 | x: 1407.75091089765, 40 | y: 286.518714806227 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /js/placementTilesData.js: -------------------------------------------------------------------------------- 1 | const placementTilesData = [ 2 | 0, 14, 0, 14, 0, 14, 0, 14, 0, 14, 0, 14, 0, 14, 0, 0, 0, 0, 0, 0, 0, 14, 0, 3 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 4 | 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5 | 14, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 14, 0, 14, 0, 0, 0, 0, 14, 0, 0, 0, 6 | 0, 0, 0, 0, 14, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 7 | 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 14, 8 | 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 9 | 0, 14, 0, 0, 0, 0, 14, 0, 0, 0, 14, 0, 14, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10 | 0, 0, 14, 0, 0, 0, 14, 0, 14, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11 | 0, 14, 0, 14, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 12 | ] 13 | -------------------------------------------------------------------------------- /js/classes/Sprite.js: -------------------------------------------------------------------------------- 1 | class Sprite { 2 | constructor({ 3 | position = { x: 0, y: 0 }, 4 | imageSrc, 5 | frames = { max: 1 }, 6 | offset = { x: 0, y: 0 } 7 | }) { 8 | this.position = position 9 | this.image = new Image() 10 | this.image.src = imageSrc 11 | this.frames = { 12 | max: frames.max, 13 | current: 0, 14 | elapsed: 0, 15 | hold: 3 16 | } 17 | this.offset = offset 18 | } 19 | 20 | draw() { 21 | const cropWidth = this.image.width / this.frames.max 22 | const crop = { 23 | position: { 24 | x: cropWidth * this.frames.current, 25 | y: 0 26 | }, 27 | width: cropWidth, 28 | height: this.image.height 29 | } 30 | c.drawImage( 31 | this.image, 32 | crop.position.x, 33 | crop.position.y, 34 | crop.width, 35 | crop.height, 36 | this.position.x + this.offset.x, 37 | this.position.y + this.offset.y, 38 | crop.width, 39 | crop.height 40 | ) 41 | } 42 | 43 | update() { 44 | // responsible for animation 45 | this.frames.elapsed++ 46 | if (this.frames.elapsed % this.frames.hold === 0) { 47 | this.frames.current++ 48 | if (this.frames.current >= this.frames.max) { 49 | this.frames.current = 0 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /js/classes/Building.js: -------------------------------------------------------------------------------- 1 | class Building extends Sprite { 2 | constructor({ position = { x: 0, y: 0 } }) { 3 | super({ 4 | position, 5 | imageSrc: './img/tower.png', 6 | frames: { 7 | max: 19 8 | }, 9 | offset: { 10 | x: 0, 11 | y: -80 12 | } 13 | }) 14 | 15 | this.width = 64 * 2 16 | this.height = 64 17 | this.center = { 18 | x: this.position.x + this.width / 2, 19 | y: this.position.y + this.height / 2 20 | } 21 | this.projectiles = [] 22 | this.radius = 250 23 | this.target 24 | } 25 | 26 | draw() { 27 | super.draw() 28 | 29 | // c.beginPath() 30 | // c.arc(this.center.x, this.center.y, this.radius, 0, Math.PI * 2) 31 | // c.fillStyle = 'rgba(0, 0, 255, 0.2)' 32 | // c.fill() 33 | } 34 | 35 | update() { 36 | this.draw() 37 | if (this.target || (!this.target && this.frames.current !== 0)) 38 | super.update() 39 | 40 | if ( 41 | this.target && 42 | this.frames.current === 6 && 43 | this.frames.elapsed % this.frames.hold === 0 44 | ) 45 | this.shoot() 46 | } 47 | 48 | shoot() { 49 | this.projectiles.push( 50 | new Projectile({ 51 | position: { 52 | x: this.center.x - 20, 53 | y: this.center.y - 110 54 | }, 55 | enemy: this.target 56 | }) 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /js/classes/Enemy.js: -------------------------------------------------------------------------------- 1 | class Enemy extends Sprite { 2 | constructor({ position = { x: 0, y: 0 } }) { 3 | super({ 4 | position, 5 | imageSrc: 'img/orc.png', 6 | frames: { 7 | max: 7 8 | } 9 | }) 10 | this.position = position 11 | this.width = 100 12 | this.height = 100 13 | this.waypointIndex = 0 14 | this.center = { 15 | x: this.position.x + this.width / 2, 16 | y: this.position.y + this.height / 2 17 | } 18 | this.radius = 50 19 | this.health = 100 20 | this.velocity = { 21 | x: 0, 22 | y: 0 23 | } 24 | } 25 | 26 | draw() { 27 | super.draw() 28 | 29 | // health bar 30 | c.fillStyle = 'red' 31 | c.fillRect(this.position.x, this.position.y - 15, this.width, 10) 32 | 33 | c.fillStyle = 'green' 34 | c.fillRect( 35 | this.position.x, 36 | this.position.y - 15, 37 | (this.width * this.health) / 100, 38 | 10 39 | ) 40 | } 41 | 42 | update() { 43 | this.draw() 44 | super.update() 45 | 46 | const waypoint = waypoints[this.waypointIndex] 47 | const yDistance = waypoint.y - this.center.y 48 | const xDistance = waypoint.x - this.center.x 49 | const angle = Math.atan2(yDistance, xDistance) 50 | 51 | const speed = 3 52 | 53 | this.velocity.x = Math.cos(angle) * speed 54 | this.velocity.y = Math.sin(angle) * speed 55 | 56 | this.position.x += this.velocity.x 57 | this.position.y += this.velocity.y 58 | 59 | this.center = { 60 | x: this.position.x + this.width / 2, 61 | y: this.position.y + this.height / 2 62 | } 63 | 64 | if ( 65 | Math.abs(Math.round(this.center.x) - Math.round(waypoint.x)) < 66 | Math.abs(this.velocity.x) && 67 | Math.abs(Math.round(this.center.y) - Math.round(waypoint.y)) < 68 | Math.abs(this.velocity.y) && 69 | this.waypointIndex < waypoints.length - 1 70 | ) { 71 | this.waypointIndex++ 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 13 | 14 |
15 | 16 |
32 | GAME OVER 33 |
34 |
49 |
61 | 62 |
63 | 69 | 70 | 73 | 74 | 75 |
100
76 |
77 | 78 |
79 | 85 | 90 | 91 |
10
92 |
93 |
94 |
95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | const canvas = document.querySelector('canvas') 2 | const c = canvas.getContext('2d') 3 | 4 | canvas.width = 1280 5 | canvas.height = 768 6 | 7 | c.fillStyle = 'white' 8 | c.fillRect(0, 0, canvas.width, canvas.height) 9 | 10 | const placementTilesData2D = [] 11 | 12 | for (let i = 0; i < placementTilesData.length; i += 20) { 13 | placementTilesData2D.push(placementTilesData.slice(i, i + 20)) 14 | } 15 | 16 | const placementTiles = [] 17 | 18 | placementTilesData2D.forEach((row, y) => { 19 | row.forEach((symbol, x) => { 20 | if (symbol === 14) { 21 | // add building placement tile here 22 | placementTiles.push( 23 | new PlacementTile({ 24 | position: { 25 | x: x * 64, 26 | y: y * 64 27 | } 28 | }) 29 | ) 30 | } 31 | }) 32 | }) 33 | 34 | const image = new Image() 35 | 36 | image.onload = () => { 37 | animate() 38 | } 39 | image.src = 'img/gameMap.png' 40 | 41 | const enemies = [] 42 | 43 | function spawnEnemies(spawnCount) { 44 | for (let i = 1; i < spawnCount + 1; i++) { 45 | const xOffset = i * 150 46 | enemies.push( 47 | new Enemy({ 48 | position: { x: waypoints[0].x - xOffset, y: waypoints[0].y } 49 | }) 50 | ) 51 | } 52 | } 53 | 54 | const buildings = [] 55 | let activeTile = undefined 56 | let enemyCount = 3 57 | let hearts = 10 58 | let coins = 100 59 | const explosions = [] 60 | spawnEnemies(enemyCount) 61 | 62 | function animate() { 63 | const animationId = requestAnimationFrame(animate) 64 | 65 | c.drawImage(image, 0, 0) 66 | 67 | for (let i = enemies.length - 1; i >= 0; i--) { 68 | const enemy = enemies[i] 69 | enemy.update() 70 | 71 | if (enemy.position.x > canvas.width) { 72 | hearts -= 1 73 | enemies.splice(i, 1) 74 | document.querySelector('#hearts').innerHTML = hearts 75 | 76 | if (hearts === 0) { 77 | console.log('game over') 78 | cancelAnimationFrame(animationId) 79 | document.querySelector('#gameOver').style.display = 'flex' 80 | } 81 | } 82 | } 83 | 84 | for (let i = explosions.length - 1; i >= 0; i--) { 85 | const explosion = explosions[i] 86 | explosion.draw() 87 | explosion.update() 88 | 89 | if (explosion.frames.current >= explosion.frames.max - 1) { 90 | explosions.splice(i, 1) 91 | } 92 | 93 | console.log(explosions) 94 | } 95 | 96 | // tracking total amount of enemies 97 | if (enemies.length === 0) { 98 | enemyCount += 2 99 | spawnEnemies(enemyCount) 100 | } 101 | 102 | placementTiles.forEach((tile) => { 103 | tile.update(mouse) 104 | }) 105 | 106 | buildings.forEach((building) => { 107 | building.update() 108 | building.target = null 109 | const validEnemies = enemies.filter((enemy) => { 110 | const xDifference = enemy.center.x - building.center.x 111 | const yDifference = enemy.center.y - building.center.y 112 | const distance = Math.hypot(xDifference, yDifference) 113 | return distance < enemy.radius + building.radius 114 | }) 115 | building.target = validEnemies[0] 116 | 117 | for (let i = building.projectiles.length - 1; i >= 0; i--) { 118 | const projectile = building.projectiles[i] 119 | 120 | projectile.update() 121 | 122 | const xDifference = projectile.enemy.center.x - projectile.position.x 123 | const yDifference = projectile.enemy.center.y - projectile.position.y 124 | const distance = Math.hypot(xDifference, yDifference) 125 | 126 | // this is when a projectile hits an enemy 127 | if (distance < projectile.enemy.radius + projectile.radius) { 128 | // enemy health and enemy removal 129 | projectile.enemy.health -= 20 130 | if (projectile.enemy.health <= 0) { 131 | const enemyIndex = enemies.findIndex((enemy) => { 132 | return projectile.enemy === enemy 133 | }) 134 | 135 | if (enemyIndex > -1) { 136 | enemies.splice(enemyIndex, 1) 137 | coins += 25 138 | document.querySelector('#coins').innerHTML = coins 139 | } 140 | } 141 | 142 | console.log(projectile.enemy.health) 143 | explosions.push( 144 | new Sprite({ 145 | position: { x: projectile.position.x, y: projectile.position.y }, 146 | imageSrc: './img/explosion.png', 147 | frames: { max: 4 }, 148 | offset: { x: 0, y: 0 } 149 | }) 150 | ) 151 | building.projectiles.splice(i, 1) 152 | } 153 | } 154 | }) 155 | } 156 | 157 | const mouse = { 158 | x: undefined, 159 | y: undefined 160 | } 161 | 162 | canvas.addEventListener('click', (event) => { 163 | if (activeTile && !activeTile.isOccupied && coins - 50 >= 0) { 164 | coins -= 50 165 | document.querySelector('#coins').innerHTML = coins 166 | buildings.push( 167 | new Building({ 168 | position: { 169 | x: activeTile.position.x, 170 | y: activeTile.position.y 171 | } 172 | }) 173 | ) 174 | activeTile.isOccupied = true 175 | buildings.sort((a, b) => { 176 | return a.position.y - b.position.y 177 | }) 178 | } 179 | }) 180 | 181 | window.addEventListener('mousemove', (event) => { 182 | mouse.x = event.clientX 183 | mouse.y = event.clientY 184 | 185 | activeTile = null 186 | for (let i = 0; i < placementTiles.length; i++) { 187 | const tile = placementTiles[i] 188 | if ( 189 | mouse.x > tile.position.x && 190 | mouse.x < tile.position.x + tile.size && 191 | mouse.y > tile.position.y && 192 | mouse.y < tile.position.y + tile.size 193 | ) { 194 | activeTile = tile 195 | break 196 | } 197 | } 198 | }) 199 | --------------------------------------------------------------------------------