├── .gitignore ├── img ├── backgroundLevel1.png ├── backgroundLevel2.png ├── backgroundLevel3.png ├── box.png ├── doorOpen.png └── king │ ├── enterDoor.png │ ├── idle.png │ ├── idleLeft.png │ ├── runLeft.png │ └── runRight.png ├── index.html ├── index.js └── js ├── classes ├── CollisionBlock.js ├── Player.js └── Sprite.js ├── data └── collisions.js ├── eventListeners.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /img/backgroundLevel1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscourses/kings-and-pigs/bb77e92e7d89937500138ef690388fe067c2a4b1/img/backgroundLevel1.png -------------------------------------------------------------------------------- /img/backgroundLevel2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscourses/kings-and-pigs/bb77e92e7d89937500138ef690388fe067c2a4b1/img/backgroundLevel2.png -------------------------------------------------------------------------------- /img/backgroundLevel3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscourses/kings-and-pigs/bb77e92e7d89937500138ef690388fe067c2a4b1/img/backgroundLevel3.png -------------------------------------------------------------------------------- /img/box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscourses/kings-and-pigs/bb77e92e7d89937500138ef690388fe067c2a4b1/img/box.png -------------------------------------------------------------------------------- /img/doorOpen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscourses/kings-and-pigs/bb77e92e7d89937500138ef690388fe067c2a4b1/img/doorOpen.png -------------------------------------------------------------------------------- /img/king/enterDoor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscourses/kings-and-pigs/bb77e92e7d89937500138ef690388fe067c2a4b1/img/king/enterDoor.png -------------------------------------------------------------------------------- /img/king/idle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscourses/kings-and-pigs/bb77e92e7d89937500138ef690388fe067c2a4b1/img/king/idle.png -------------------------------------------------------------------------------- /img/king/idleLeft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscourses/kings-and-pigs/bb77e92e7d89937500138ef690388fe067c2a4b1/img/king/idleLeft.png -------------------------------------------------------------------------------- /img/king/runLeft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscourses/kings-and-pigs/bb77e92e7d89937500138ef690388fe067c2a4b1/img/king/runLeft.png -------------------------------------------------------------------------------- /img/king/runRight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscourses/kings-and-pigs/bb77e92e7d89937500138ef690388fe067c2a4b1/img/king/runRight.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const canvas = document.querySelector('canvas') 2 | const c = canvas.getContext('2d') 3 | 4 | canvas.width = 64 * 16 // 1024 5 | canvas.height = 64 * 9 // 576 6 | 7 | let parsedCollisions 8 | let collisionBlocks 9 | let background 10 | let doors 11 | const player = new Player({ 12 | imageSrc: './img/king/idle.png', 13 | frameRate: 11, 14 | animations: { 15 | idleRight: { 16 | frameRate: 11, 17 | frameBuffer: 2, 18 | loop: true, 19 | imageSrc: './img/king/idle.png', 20 | }, 21 | idleLeft: { 22 | frameRate: 11, 23 | frameBuffer: 2, 24 | loop: true, 25 | imageSrc: './img/king/idleLeft.png', 26 | }, 27 | runRight: { 28 | frameRate: 8, 29 | frameBuffer: 4, 30 | loop: true, 31 | imageSrc: './img/king/runRight.png', 32 | }, 33 | runLeft: { 34 | frameRate: 8, 35 | frameBuffer: 4, 36 | loop: true, 37 | imageSrc: './img/king/runLeft.png', 38 | }, 39 | enterDoor: { 40 | frameRate: 8, 41 | frameBuffer: 4, 42 | loop: false, 43 | imageSrc: './img/king/enterDoor.png', 44 | onComplete: () => { 45 | console.log('completed animation') 46 | gsap.to(overlay, { 47 | opacity: 1, 48 | onComplete: () => { 49 | level++ 50 | 51 | if (level === 4) level = 1 52 | levels[level].init() 53 | player.switchSprite('idleRight') 54 | player.preventInput = false 55 | gsap.to(overlay, { 56 | opacity: 0, 57 | }) 58 | }, 59 | }) 60 | }, 61 | }, 62 | }, 63 | }) 64 | 65 | let level = 3 66 | let levels = { 67 | 1: { 68 | init: () => { 69 | parsedCollisions = collisionsLevel1.parse2D() 70 | collisionBlocks = parsedCollisions.createObjectsFrom2D() 71 | player.collisionBlocks = collisionBlocks 72 | if (player.currentAnimation) player.currentAnimation.isActive = false 73 | 74 | background = new Sprite({ 75 | position: { 76 | x: 0, 77 | y: 0, 78 | }, 79 | imageSrc: './img/backgroundLevel1.png', 80 | }) 81 | 82 | doors = [ 83 | new Sprite({ 84 | position: { 85 | x: 767, 86 | y: 270, 87 | }, 88 | imageSrc: './img/doorOpen.png', 89 | frameRate: 5, 90 | frameBuffer: 5, 91 | loop: false, 92 | autoplay: false, 93 | }), 94 | ] 95 | }, 96 | }, 97 | 2: { 98 | init: () => { 99 | parsedCollisions = collisionsLevel2.parse2D() 100 | collisionBlocks = parsedCollisions.createObjectsFrom2D() 101 | player.collisionBlocks = collisionBlocks 102 | player.position.x = 96 103 | player.position.y = 140 104 | 105 | if (player.currentAnimation) player.currentAnimation.isActive = false 106 | 107 | background = new Sprite({ 108 | position: { 109 | x: 0, 110 | y: 0, 111 | }, 112 | imageSrc: './img/backgroundLevel2.png', 113 | }) 114 | 115 | doors = [ 116 | new Sprite({ 117 | position: { 118 | x: 772.0, 119 | y: 336, 120 | }, 121 | imageSrc: './img/doorOpen.png', 122 | frameRate: 5, 123 | frameBuffer: 5, 124 | loop: false, 125 | autoplay: false, 126 | }), 127 | ] 128 | }, 129 | }, 130 | 3: { 131 | init: () => { 132 | parsedCollisions = collisionsLevel3.parse2D() 133 | collisionBlocks = parsedCollisions.createObjectsFrom2D() 134 | player.collisionBlocks = collisionBlocks 135 | player.position.x = 750 136 | player.position.y = 230 137 | if (player.currentAnimation) player.currentAnimation.isActive = false 138 | 139 | background = new Sprite({ 140 | position: { 141 | x: 0, 142 | y: 0, 143 | }, 144 | imageSrc: './img/backgroundLevel3.png', 145 | }) 146 | 147 | doors = [ 148 | new Sprite({ 149 | position: { 150 | x: 176.0, 151 | y: 335, 152 | }, 153 | imageSrc: './img/doorOpen.png', 154 | frameRate: 5, 155 | frameBuffer: 5, 156 | loop: false, 157 | autoplay: false, 158 | }), 159 | ] 160 | }, 161 | }, 162 | } 163 | 164 | const keys = { 165 | w: { 166 | pressed: false, 167 | }, 168 | a: { 169 | pressed: false, 170 | }, 171 | d: { 172 | pressed: false, 173 | }, 174 | } 175 | 176 | const overlay = { 177 | opacity: 0, 178 | } 179 | 180 | function animate() { 181 | window.requestAnimationFrame(animate) 182 | 183 | background.draw() 184 | // collisionBlocks.forEach((collisionBlock) => { 185 | // collisionBlock.draw() 186 | // }) 187 | 188 | doors.forEach((door) => { 189 | door.draw() 190 | }) 191 | 192 | player.handleInput(keys) 193 | player.draw() 194 | player.update() 195 | 196 | c.save() 197 | c.globalAlpha = overlay.opacity 198 | c.fillStyle = 'black' 199 | c.fillRect(0, 0, canvas.width, canvas.height) 200 | c.restore() 201 | } 202 | 203 | levels[level].init() 204 | animate() 205 | -------------------------------------------------------------------------------- /js/classes/CollisionBlock.js: -------------------------------------------------------------------------------- 1 | class CollisionBlock { 2 | constructor({ position }) { 3 | this.position = position 4 | this.width = 64 5 | this.height = 64 6 | } 7 | 8 | draw() { 9 | c.fillStyle = 'rgba(255, 0, 0, 0.5)' 10 | c.fillRect(this.position.x, this.position.y, this.width, this.height) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /js/classes/Player.js: -------------------------------------------------------------------------------- 1 | class Player extends Sprite { 2 | constructor({ collisionBlocks = [], imageSrc, frameRate, animations, loop }) { 3 | super({ imageSrc, frameRate, animations, loop }) 4 | this.position = { 5 | x: 200, 6 | y: 200, 7 | } 8 | 9 | this.velocity = { 10 | x: 0, 11 | y: 0, 12 | } 13 | 14 | this.sides = { 15 | bottom: this.position.y + this.height, 16 | } 17 | this.gravity = 1 18 | 19 | this.collisionBlocks = collisionBlocks 20 | } 21 | 22 | update() { 23 | // this is the blue box 24 | // c.fillStyle = 'rgba(0, 0, 255, 0.5)' 25 | // c.fillRect(this.position.x, this.position.y, this.width, this.height) 26 | 27 | this.position.x += this.velocity.x 28 | 29 | this.updateHitbox() 30 | 31 | this.checkForHorizontalCollisions() 32 | this.applyGravity() 33 | 34 | this.updateHitbox() 35 | 36 | // c.fillRect( 37 | // this.hitbox.position.x, 38 | // this.hitbox.position.y, 39 | // this.hitbox.width, 40 | // this.hitbox.height 41 | // ) 42 | this.checkForVerticalCollisions() 43 | } 44 | 45 | handleInput(keys) { 46 | if (this.preventInput) return 47 | this.velocity.x = 0 48 | if (keys.d.pressed) { 49 | this.switchSprite('runRight') 50 | this.velocity.x = 5 51 | this.lastDirection = 'right' 52 | } else if (keys.a.pressed) { 53 | this.switchSprite('runLeft') 54 | this.velocity.x = -5 55 | this.lastDirection = 'left' 56 | } else { 57 | if (this.lastDirection === 'left') this.switchSprite('idleLeft') 58 | else this.switchSprite('idleRight') 59 | } 60 | } 61 | 62 | switchSprite(name) { 63 | if (this.image === this.animations[name].image) return 64 | this.currentFrame = 0 65 | this.image = this.animations[name].image 66 | this.frameRate = this.animations[name].frameRate 67 | this.frameBuffer = this.animations[name].frameBuffer 68 | this.loop = this.animations[name].loop 69 | this.currentAnimation = this.animations[name] 70 | } 71 | 72 | updateHitbox() { 73 | this.hitbox = { 74 | position: { 75 | x: this.position.x + 58, 76 | y: this.position.y + 34, 77 | }, 78 | width: 50, 79 | height: 53, 80 | } 81 | } 82 | 83 | checkForHorizontalCollisions() { 84 | for (let i = 0; i < this.collisionBlocks.length; i++) { 85 | const collisionBlock = this.collisionBlocks[i] 86 | 87 | // if a collision exists 88 | if ( 89 | this.hitbox.position.x <= 90 | collisionBlock.position.x + collisionBlock.width && 91 | this.hitbox.position.x + this.hitbox.width >= 92 | collisionBlock.position.x && 93 | this.hitbox.position.y + this.hitbox.height >= 94 | collisionBlock.position.y && 95 | this.hitbox.position.y <= 96 | collisionBlock.position.y + collisionBlock.height 97 | ) { 98 | // collision on x axis going to the left 99 | if (this.velocity.x < -0) { 100 | const offset = this.hitbox.position.x - this.position.x 101 | this.position.x = 102 | collisionBlock.position.x + collisionBlock.width - offset + 0.01 103 | break 104 | } 105 | 106 | if (this.velocity.x > 0) { 107 | const offset = 108 | this.hitbox.position.x - this.position.x + this.hitbox.width 109 | this.position.x = collisionBlock.position.x - offset - 0.01 110 | break 111 | } 112 | } 113 | } 114 | } 115 | 116 | applyGravity() { 117 | this.velocity.y += this.gravity 118 | this.position.y += this.velocity.y 119 | } 120 | 121 | checkForVerticalCollisions() { 122 | for (let i = 0; i < this.collisionBlocks.length; i++) { 123 | const collisionBlock = this.collisionBlocks[i] 124 | 125 | // if a collision exists 126 | if ( 127 | this.hitbox.position.x <= 128 | collisionBlock.position.x + collisionBlock.width && 129 | this.hitbox.position.x + this.hitbox.width >= 130 | collisionBlock.position.x && 131 | this.hitbox.position.y + this.hitbox.height >= 132 | collisionBlock.position.y && 133 | this.hitbox.position.y <= 134 | collisionBlock.position.y + collisionBlock.height 135 | ) { 136 | if (this.velocity.y < 0) { 137 | this.velocity.y = 0 138 | const offset = this.hitbox.position.y - this.position.y 139 | this.position.y = 140 | collisionBlock.position.y + collisionBlock.height - offset + 0.01 141 | break 142 | } 143 | 144 | if (this.velocity.y > 0) { 145 | this.velocity.y = 0 146 | const offset = 147 | this.hitbox.position.y - this.position.y + this.hitbox.height 148 | this.position.y = collisionBlock.position.y - offset - 0.01 149 | break 150 | } 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /js/classes/Sprite.js: -------------------------------------------------------------------------------- 1 | class Sprite { 2 | constructor({ 3 | position, 4 | imageSrc, 5 | frameRate = 1, 6 | animations, 7 | frameBuffer = 2, 8 | loop = true, 9 | autoplay = true, 10 | }) { 11 | this.position = position 12 | this.image = new Image() 13 | this.image.onload = () => { 14 | this.loaded = true 15 | this.width = this.image.width / this.frameRate 16 | this.height = this.image.height 17 | } 18 | this.image.src = imageSrc 19 | this.loaded = false 20 | this.frameRate = frameRate 21 | this.currentFrame = 0 22 | this.elapsedFrames = 0 23 | this.frameBuffer = frameBuffer 24 | this.animations = animations 25 | this.loop = loop 26 | this.autoplay = autoplay 27 | this.currentAnimation 28 | 29 | if (this.animations) { 30 | for (let key in this.animations) { 31 | const image = new Image() 32 | image.src = this.animations[key].imageSrc 33 | this.animations[key].image = image 34 | } 35 | } 36 | } 37 | draw() { 38 | if (!this.loaded) return 39 | const cropbox = { 40 | position: { 41 | x: this.width * this.currentFrame, 42 | y: 0, 43 | }, 44 | width: this.width, 45 | height: this.height, 46 | } 47 | 48 | c.drawImage( 49 | this.image, 50 | cropbox.position.x, 51 | cropbox.position.y, 52 | cropbox.width, 53 | cropbox.height, 54 | this.position.x, 55 | this.position.y, 56 | this.width, 57 | this.height 58 | ) 59 | 60 | this.updateFrames() 61 | } 62 | 63 | play() { 64 | this.autoplay = true 65 | } 66 | 67 | updateFrames() { 68 | if (!this.autoplay) return 69 | 70 | this.elapsedFrames++ 71 | 72 | if (this.elapsedFrames % this.frameBuffer === 0) { 73 | if (this.currentFrame < this.frameRate - 1) this.currentFrame++ 74 | else if (this.loop) this.currentFrame = 0 75 | } 76 | 77 | if (this.currentAnimation?.onComplete) { 78 | if ( 79 | this.currentFrame === this.frameRate - 1 && 80 | !this.currentAnimation.isActive 81 | ) { 82 | this.currentAnimation.onComplete() 83 | this.currentAnimation.isActive = true 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /js/data/collisions.js: -------------------------------------------------------------------------------- 1 | const collisionsLevel1 = [ 2 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3 | 0, 0, 0, 0, 0, 0, 0, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 4 | 292, 292, 292, 0, 0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 292, 0, 0, 292, 5 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 292, 0, 0, 292, 292, 0, 0, 0, 0, 0, 0, 0, 6 | 0, 0, 0, 0, 292, 0, 0, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 7 | 292, 292, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9 | ] 10 | 11 | const collisionsLevel2 = [ 12 | 292, 292, 292, 292, 292, 292, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 292, 0, 0, 0, 0, 13 | 0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 292, 0, 0, 0, 0, 0, 292, 0, 0, 0, 0, 0, 0, 14 | 0, 0, 0, 292, 292, 292, 292, 0, 0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15 | 292, 0, 0, 292, 0, 0, 292, 292, 292, 292, 292, 292, 0, 0, 292, 292, 292, 0, 0, 16 | 292, 292, 292, 292, 0, 0, 0, 0, 292, 0, 0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17 | 0, 0, 292, 0, 0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 292, 292, 292, 292, 292, 0, 0, 18 | 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 0, 0, 0, 0, 19 | ] 20 | 21 | const collisionsLevel3 = [ 22 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 250, 250, 250, 250, 250, 23 | 250, 250, 250, 250, 250, 250, 250, 250, 250, 0, 0, 250, 0, 0, 0, 0, 0, 0, 0, 24 | 0, 0, 0, 0, 0, 250, 0, 0, 250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 250, 0, 0, 25 | 250, 0, 0, 0, 0, 0, 0, 0, 0, 250, 250, 250, 250, 250, 0, 0, 250, 0, 0, 0, 0, 26 | 0, 0, 0, 0, 0, 0, 0, 250, 0, 0, 0, 250, 250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 250, 27 | 250, 0, 0, 0, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 28 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29 | ] 30 | -------------------------------------------------------------------------------- /js/eventListeners.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('keydown', (event) => { 2 | if (player.preventInput) return 3 | switch (event.key) { 4 | case 'w': 5 | for (let i = 0; i < doors.length; i++) { 6 | const door = doors[i] 7 | 8 | if ( 9 | player.hitbox.position.x + player.hitbox.width <= 10 | door.position.x + door.width && 11 | player.hitbox.position.x >= door.position.x && 12 | player.hitbox.position.y + player.hitbox.height >= door.position.y && 13 | player.hitbox.position.y <= door.position.y + door.height 14 | ) { 15 | player.velocity.x = 0 16 | player.velocity.y = 0 17 | player.preventInput = true 18 | player.switchSprite('enterDoor') 19 | door.play() 20 | return 21 | } 22 | } 23 | if (player.velocity.y === 0) player.velocity.y = -25 24 | 25 | break 26 | case 'a': 27 | // move player to the left 28 | keys.a.pressed = true 29 | break 30 | case 'd': 31 | // move player to the right 32 | keys.d.pressed = true 33 | break 34 | } 35 | }) 36 | 37 | window.addEventListener('keyup', (event) => { 38 | switch (event.key) { 39 | case 'a': 40 | // move player to the left 41 | keys.a.pressed = false 42 | 43 | break 44 | case 'd': 45 | // move player to the right 46 | keys.d.pressed = false 47 | 48 | break 49 | } 50 | }) 51 | -------------------------------------------------------------------------------- /js/utils.js: -------------------------------------------------------------------------------- 1 | Array.prototype.parse2D = function () { 2 | const rows = [] 3 | for (let i = 0; i < this.length; i += 16) { 4 | rows.push(this.slice(i, i + 16)) 5 | } 6 | 7 | return rows 8 | } 9 | 10 | Array.prototype.createObjectsFrom2D = function () { 11 | const objects = [] 12 | this.forEach((row, y) => { 13 | row.forEach((symbol, x) => { 14 | if (symbol === 292 || symbol === 250) { 15 | // push a new collision into collisionblocks array 16 | objects.push( 17 | new CollisionBlock({ 18 | position: { 19 | x: x * 64, 20 | y: y * 64, 21 | }, 22 | }) 23 | ) 24 | } 25 | }) 26 | }) 27 | 28 | return objects 29 | } 30 | --------------------------------------------------------------------------------