diff --git a/.eslintignore b/.eslintignore index 38972655f..332513817 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,10 +4,12 @@ node_modules /.svelte-kit /package .env -.env.* +.env.\* !.env.example +src/lib/components/error-game/js # Ignore files for PNPM, NPM and YARN + pnpm-lock.yaml package-lock.json yarn.lock diff --git a/.prettierignore b/.prettierignore index 38972655f..ec7620de4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,6 +6,7 @@ node_modules .env .env.* !.env.example +src/lib/components/error-game/js # Ignore files for PNPM, NPM and YARN pnpm-lock.yaml diff --git a/src/lib/components/error-game/game.svelte b/src/lib/components/error-game/game.svelte new file mode 100644 index 000000000..ce814ecfc --- /dev/null +++ b/src/lib/components/error-game/game.svelte @@ -0,0 +1,74 @@ + + + + + + + + + +Start + + diff --git a/src/lib/components/error-game/game.ts b/src/lib/components/error-game/game.ts new file mode 100644 index 000000000..7d2b2403d --- /dev/null +++ b/src/lib/components/error-game/game.ts @@ -0,0 +1,271 @@ +import { Capy } from './js/capy.js'; +import { InputHandler } from './js/input.js'; +import { Background } from './js/background.js'; +import { GroundMob, FlyingMob, Hedgehog, Wizard } from './js/mobs.js'; +import { UI } from './js/UI.js'; +import { loadImages, images, isMobileDevice } from './js/shared.js'; + +let lastTime = 0; + +export async function setup(canvas: HTMLCanvasElement, button: HTMLButtonElement) { + const ctx = canvas.getContext('2d'); + if (ctx === null) { + throw new Error('2d context not supported'); + } + + canvas.width = 1000; + canvas.height = 550; + + // Load images before setting up the game + loadImages(); + + // Wait until all images are loaded + await new Promise((resolve) => { + const checkImagesLoaded = setInterval(() => { + if (Object.keys(images).length === 13) { // Adjust this number based on the number of images in `imageArr` + clearInterval(checkImagesLoaded); + resolve(); + } + }, 100); + }); + + const game = new Game(canvas.width, canvas.height, ctx); + game.setupButtonClick = game.setupButtonClick.bind(game); + + window.addEventListener('keydown', (event) => { + if (event.code === 'Enter') { + event.preventDefault(); + if (game.gameOver) { + game.restart(); + } else { + game.togglePause(); + } + } + }); + + canvas.addEventListener('click', () => { + game.togglePause(); + }); + + button.addEventListener('click', game.setupButtonClick); + + game.draw(); +} + +// All logic will go through class Game +class Game { + gameOver: boolean; + isFirstPause: boolean; + animationId: null | number; + isPaused: boolean; + width: number; + height: number; + groundMargin: number; + speed: number; + maxSpeed: number; + maxParticles: number; + background: Background; + capy: Capy; + input: InputHandler; + UI: UI; + mobs: (GroundMob | FlyingMob | Hedgehog | Wizard)[]; + particles: any[]; + collisions: any[]; + floatingText: any[]; + mobTimer: number; + mobInterval: number; + debug: boolean; + health: number; + energy: number; + energyInc: number; + energyDec: boolean; + score: number; + highscore: number; + hedgehogScore: number; + beeScore: number; + fontColor: string; + hudHeight: number; + ctx: CanvasRenderingContext2D; + + constructor(width: number, height: number, ctx: CanvasRenderingContext2D) { + this.isFirstPause = true; + this.animationId = null; + this.isPaused = true; + this.width = width; + this.height = height; + this.groundMargin = 30; + this.speed = 0; + this.maxSpeed = 3; + this.maxParticles = 50; + this.background = new Background(this); + this.capy = new Capy(this); + this.input = new InputHandler(this); + this.UI = new UI(this); + this.mobs = []; + this.particles = []; + this.collisions = []; + this.floatingText = []; + this.mobTimer = 0; + this.mobInterval = 1000; + this.debug = false; + this.gameOver = false; + this.health = 6; + this.energy = 6; + this.energyInc = 1200; + this.energyDec = false; + this.score = 0; + this.highscore = getHighscore(); + this.hedgehogScore = 0; + this.beeScore = 0; + this.fontColor = 'black'; + this.hudHeight = 50; + this.capy.currentState = this.capy.states[0]; // points to index within this.states + this.capy.currentState.enter(); // activate initial default state + this.ctx = ctx; + } + + togglePause() { + this.isPaused = !this.isPaused; + if (!this.isPaused && this.isFirstPause) { + this.isFirstPause = false; + } + + if (this.isPaused) { + if (this.animationId) { + cancelAnimationFrame(this.animationId); + } + this.draw(); + } else { + this.animate(0); + } + } + + // Run forever animation frame + update(delta: number) { + this.energyDec = false; + if (this.isPaused) return; + if (this.health === 0) this.gameOver = true; + if (this.gameOver) { + this.displayHighScore(); + } + this.background.update(); + this.capy.update(this.input.keys, delta); + + // Handle Mobs + if (this.mobTimer > this.mobInterval) { + this.addMob(); + this.mobTimer = 0; + } else { + this.mobTimer += delta; + } + + // Handle Mobs + this.mobs.forEach((mob) => { + mob.update(delta); + }); + + // Handle Floating Text + this.floatingText.forEach((text) => { + text.update(); + }); + + // Handle Particles + this.particles.forEach((particle) => { + particle.update(); + }); + if (this.particles.length > this.maxParticles) { + this.particles.length = this.maxParticles; + } + + // Handle collision boom + this.collisions.forEach((collision) => { + collision.update(delta); + }); + + this.mobs = this.mobs.filter((mob) => !mob.markedForDeletion); + this.floatingText = this.floatingText.filter((text) => !text.markedForDeletion); + this.particles = this.particles.filter((particle) => !particle.markedForDeletion); + this.collisions = this.collisions.filter((collision) => !collision.markedForDeletion); + if (this.energyInc <= 1200) this.energyInc += 1; + if (this.energyInc % 200 === 0 && !this.energyDec) { + this.energy += 1; + } + } + + // Restart Game + restart() { + this.gameOver = false; + this.capy.restart(); + this.background.restart(); + this.mobs = []; + this.collisions = []; + this.health = 6; + this.hedgehogScore = 0; + this.beeScore = 0; + this.score = 0; + this.isPaused = false; // Set isPaused to false so the game isn't paused after restarting + this.animate(0); + } + + setupButtonClick() { + this.capy.mobileJump(); + } + + // Handle Local High Score + displayHighScore() { + this.highscore = getHighscore(); + + if (this.gameOver && (this.highscore === 0 || this.score >= this.highscore)) { + localStorage.setItem('highscore', this.score.toString()); + this.highscore = this.score; + } + console.log(this.highscore); + } + + // Draw images, score, and so on + draw() { + this.background.draw(this.ctx); + this.capy.draw(this.ctx); + this.mobs.forEach((mob) => { + mob.draw(this.ctx); + }); + this.particles.forEach((particle) => { + particle.draw(this.ctx); + }); + this.collisions.forEach((collision) => { + collision.draw(this.ctx); + }); + this.floatingText.forEach((text) => { + text.draw(this.ctx); + }); + this.UI.draw(this.ctx); + } + + addMob() { + if (isMobileDevice()) { + this.mobs.push(new FlyingMob(this)); + if (this.speed > 0 && Math.random() < 0.3) + this.mobs.push(new GroundMob(this), new Hedgehog(this)); + } else { + this.mobs.push(new FlyingMob(this)); + if (this.speed > 0 && Math.random() < 0.5) this.mobs.push(new Wizard(this)); + if (this.speed > 0 && Math.random() < 1) + this.mobs.push(new GroundMob(this), new Hedgehog(this)); + } + } + + animate(time: number) { + const delta = time - lastTime; + lastTime = time; + this.ctx.clearRect(0, 0, this.width, this.height); + this.update(delta); + this.draw(); + if (!this.gameOver) { + this.animationId = requestAnimationFrame(this.animate.bind(this)); + } + } +} + +function getHighscore() { + return parseInt(localStorage.getItem('highscore')!, 10) || 0; +} diff --git a/src/lib/components/error-game/js/UI.js b/src/lib/components/error-game/js/UI.js new file mode 100644 index 000000000..687a56a38 --- /dev/null +++ b/src/lib/components/error-game/js/UI.js @@ -0,0 +1,87 @@ +import { images } from './shared.js'; + +export class UI { + constructor(game){ + this.game = game; + this.fontSize = 30; + this.fontFamily = 'Luckiest Guy'; + } + + draw(context){ + context.save() + context.shadowOffsetX = 2; + context.shadowOffsetY = 2; + context.shadowColor = 'white'; + context.ShadowBlur = 0; + context.font = this.fontSize + 'px ' + this.fontFamily; + context.textAlign = 'left'; + context.fillStyle = this.game.fontColor; + + // score + const scoreXOffset = 785; + context.fillText('Points: ' + this.game.score, scoreXOffset, 35) // .fillText() - draws filled text on the canvas + + // draw hedgehog count + const hedgehogCountXOffset = 650; + context.drawImage(images.hedgehogImage, 0, 0, 22, 21, hedgehogCountXOffset, 15, 24, 27); + context.fillText('x ' + this.game.hedgehogScore, 35 + hedgehogCountXOffset, 35); + + // draw bee count + const beeCountXOffset = 535; + context.drawImage(images.beeImage, 26, 0, 50 - 26, 27 - 0, beeCountXOffset, 10, 50 - 26, 27 - 0); + context.fillText('x ' + this.game.beeScore, 32 + beeCountXOffset, 35); + + // draw the health and energy bars + const drawBar = (type) => { + let yDist; + let barX; + if (type === "health") { + yDist = 0; + barX = 30; + } + else if (type === "energy") { + yDist = 32; + barX = 120; + } + else { + yDist = 0; + barX = 0; + } + const barWidth = 32; + let frameNum = Math.max(0, 6 - this.game[type]); + let frame = frameNum * barWidth; + if (this.game[type] < 6) frame -= barWidth; + context.drawImage(images.bars, 64 + frame, yDist, 31, 18, barX, -14, 96, 60); + if (this.game[type] === 6) context.drawImage(images.bars, 51, yDist, 6, 18, 65 + barX, -14, 18, 60); + } + drawBar("health"); + drawBar("energy"); + + if (this.game.isPaused && !this.game.gameOver) { + context.textAlign = 'center'; + context.font = this.fontSize * 2 + 'px ' + this.fontFamily; + + if (this.game.isFirstPause) { + context.font = this.fontSize * 0.8 + context.fillText('Press Enter or Tap to Start', this.game.width / 2, this.game.height / 2 - 20); + } else { + context.font = this.fontSize * 0.8 + context.fillText('Press Enter or Tap to Start/Pause', this.game.width / 2, this.game.height / 2 + 20); + } + + + } + // game over + if (this.game.gameOver){ + context.textAlign = 'center'; + context.font = this.fontSize * 2 + 'px ' + this.fontFamily; + context.fillText(`You ran out of health!`, this.game.width * 0.5, this.game.height * 0.5 - 20); + context.font = this.fontSize * 1.5 + 'px ' + this.fontFamily; + context.fillText('Press Enter or Swipe Down to Restart!', this.game.width * 0.5, this.game.height * 0.5 + 25); + context.font = this.fontSize * 0.8 + 'px ' + this.fontFamily; + context.fillText(`Your final score is: ${this.game.score}`, this.game.width * 0.5, this.game.height * 0.5 + 55); + context.fillText(`Your highscore is ${this.game.highscore}`, this.game.width * 0.5, this.game.height * 0.5 + 80); + } + context.restore(); + } +} \ No newline at end of file diff --git a/src/lib/components/error-game/js/background.js b/src/lib/components/error-game/js/background.js new file mode 100644 index 000000000..e01cb0ef1 --- /dev/null +++ b/src/lib/components/error-game/js/background.js @@ -0,0 +1,53 @@ +import { images } from "./shared.js"; + +class Layer { + constructor(game, width, height, speedModifier, image) { + this.game = game; + this.width = width; + this.height = height; + this.speedModifier = speedModifier; + this.image = image; + this.x = 0; + this.y = 50; //should be game.hudHeight + } + update(){ + if (this.x < -this.width) this.x = 0 + else this.x -= this.game.speed * this.speedModifier + } + draw(context){ + context.drawImage(this.image, this.x, this.y, this.width, this.height); + context.drawImage(this.image, this.x + this.width, this.y, this.width, this.height); + } + +} + +export class Background { + constructor(game){ + this.game = game; + this.width = 1000; + this.height = 500; + this.layer1 = images.layer1; + this.layer2 = images.layer2; + this.layer3 = images.layer3; + this.layer4 = images.layer4; + this.layer1 = new Layer(this.game, this.width, this.height, 0, this.layer1); + this.layer2 = new Layer(this.game, this.width, this.height, 0.3, this.layer2); + this.layer3 = new Layer(this.game, this.width, this.height, 0.8, this.layer3); + this.layer4 = new Layer(this.game, this.width, this.height, 1, this.layer4); + this.backgroundLayers = [this.layer1, this.layer2, this.layer3, this.layer4]; + } + update(){ + this.backgroundLayers.forEach(layer => { + layer.update() + }) + } + draw(context){ + this.backgroundLayers.forEach(layer => { + layer.draw(context) + }) + } + restart(){ + this.x = 0; + } +} + diff --git a/src/lib/components/error-game/js/capy.js b/src/lib/components/error-game/js/capy.js new file mode 100644 index 000000000..a29df3374 --- /dev/null +++ b/src/lib/components/error-game/js/capy.js @@ -0,0 +1,136 @@ +import { Sitting, Walking, Jumping, Falling, Charging, Slamming, Hurt } from './capyStates.js' +import { Boom } from './collision.js' +import { FloatingText } from './floatingText.js'; +import { images } from './shared.js'; + +export class Capy { + constructor(game){ + this.game = game; + this.width = 64; + this.height = 42.5; + this.x = 0; + this.image = images.capyImage; + this.maxFrame; + this.fps = 20 + this.frameInterval = 1000/this.fps; + this.frameTimer = 0; + // Canvas.height - this.height + this.y = this.game.height - this.height - this.game.groundMargin + this.speed = 0; + this.frameX = 0; + this.frameY = 0; + this.maxSpeed = 10; + this.speedY = 0; + this.gravity = 1; + this.states = [new Sitting(this.game), new Walking(this.game), new Jumping(this.game), new Falling(this.game), new Charging(this.game), new Slamming(this.game), new Hurt(this.game)]; + this.currentState = null; + } + + mobileJump(){ + this.setState(2, 1) + } + + restart(){ + this.x = 0; + this.y = this.game.height - this.height - this.game.groundMargin; + this.setState(0, 0) + } + + update(input, delta){ + this.checkCollision(); + this.currentState.handleInput(input) + // Horizontal movement + this.x += this.speed; + // .includes() - method determines whether an array includes a certain + // value among its entries, returning true or false as appropriate + if (input.includes('ArrowRight') && this.currentState !== this.states[6] ) this.speed = this.maxSpeed; + else if (input.includes('ArrowLeft') && this.currentState !== this.states[6]) this.speed = -this.maxSpeed; + else this.speed = 0; + // If player is not pressing anything, Capy stops moving + if (this.x < 0) this.x = 0; + // Player can't move off screen + if (this.x > this.game.width - this.width) this.x = this.game.width - this.width; + + // Vertical movement + this.y += this.speedY + if (!this.onGround()) this.speedY += this.gravity; + else this.speedY = 0; + // Player animation + if (this.frameTimer > this.frameInterval){ + this.frameTimer = 0; + if (this.frameX < this.maxFrame) this.frameX++; + else this.frameX = 0 + } else { + this.frameTimer += delta; + } + + } + draw(context){ + if (this.game.debug) context.strokeRect(this.x, this.y, this.width, this.height) + context.drawImage(this.image, this.frameX * this.width, this.frameY * this.height, this.width, this.height, this.x, this.y, this.width, this.height) + } + onGround(){ + // Check if the player is off the ground or not + return this.y >= this.game.height - this.height - this.game.groundMargin; + } + // Allows us to switch Capy between states + setState(state, speed){ + this.currentState = this.states[state]; + this.game.speed = this.game.maxSpeed * speed; + this.currentState.enter(); + } + checkCollision(){ + this.game.mobs.forEach(mob => { + if ( + // collision detected + mob.x < this.x + this.width && + mob.x + mob.width > this.x && + mob.y < this.y + this.height && + mob.y + mob.height > this.y + ){ + mob.markedForDeletion = true; + this.game.collisions.push(new Boom(this.game, mob.x + mob.width * 0.5, mob.y + mob.height * 0.5)) + + switch (mob.name) { + case "bee": + this.game.beeScore++; + this.game.floatingText.push(new FloatingText('+1', mob.x, mob.y, 945, 50)) + this.game.score++; + break; + case "hedgehog": + this.game.hedgehogScore++; + this.game.score += 2; + this.game.floatingText.push(new FloatingText('+2', mob.x, mob.y, 945, 50)) + break; + case "wolf": + if (this.currentState === this.states[4] || this.currentState === this.states[5]){ + this.game.score++; + this.game.floatingText.push(new FloatingText('+1', mob.x, mob.y, 945, 50)) + } else { + this.setState(6, 0) + if (this.game.health > 0) + this.game.health--; + } + break; + case "wizard": + if (this.currentState === this.states[4] || this.currentState === this.states[5]){ + this.game.score ++ + this.game.floatingText.push(new FloatingText('+2', mob.x, mob.y, 945, 50)) + } else { + this.setState(6, 0) + if (this.game.health > 0) + this.game.health--; + } + break; + default: + console.error("mob type not detected"); + break; + } + } else { + // no collision + } + }) + } +} + + diff --git a/src/lib/components/error-game/js/capyStates.js b/src/lib/components/error-game/js/capyStates.js new file mode 100644 index 000000000..fbbc36ec7 --- /dev/null +++ b/src/lib/components/error-game/js/capyStates.js @@ -0,0 +1,191 @@ +import { Dust, Fire, Gravity, AOE } from './particles.js' + +const states = { + // ENUM OBJECT - pair values and names of each state, helps with code readability + SITTING: 0, + WALKING: 1, + JUMPING: 2, + FALLING: 3, + CHARGING: 4, + SLAMMING: 5, + HURT: 6, +} + +class State { // SUPER Class + constructor(state, game){ + this.state = state; + this.game = game; + } +} + +export class Sitting extends State { // Child Class (sub class) + constructor(game){ + super('SITTING', game) + + } + enter(){ + this.frameX = 0; + this.game.capy.maxFrame = 7; + this.game.capy.frameY = 5; + + } + // Switch the game.capy into different states + handleInput(input){ + // While a game.capy is in a certain state, it will only react to a certain amount of inputs + if (input.includes('ArrowRight')){ + this.game.capy.setState(states.WALKING, 1); + } else if (input.includes(' ')) { + this.game.capy.setState(states.CHARGING, 2) + } + } +} +// each row is 1.5 so if you want to a specific row then do it +export class Walking extends State { // Child Class (sub class) + constructor(game){ + super('WALKING', game) + + } + enter(){ + this.game.capy.frameX = 0; + this.game.capy.maxFrame = 7; + this.game.capy.frameY = 12.5; + } + // Switch the game.capy into different states + handleInput(input){ + this.game.particles.push(new Dust(this.game, this.game.capy.x + this.game.capy.width * 0.5, this.game.capy.y + this.game.capy.height)); + // While a game.capy is in a certain state, it will only react to a certain amount of inputs + if (input.includes('ArrowDown')){ + this.game.capy.setState(states.SITTING, 0); + } else if (input.includes('ArrowUp')) { + this.game.capy.setState(states.JUMPING, 1); + } else if (input.includes(' ') && this.game.capy.onGround() && this.game.energy > 0) { + this.game.capy.setState(states.CHARGING, 2) + } + } +} + +export class Jumping extends State { // Child Class (sub class) + constructor(game){ + super('JUMPING', game) + + } + enter(){ + if (this.game.capy.onGround()) this.game.capy.speedY -= 29; + this.frameX = 0; + this.game.capy.maxFrame = 2; + this.game.capy.frameY = 5; + } + // Switch the game.capy into different states + handleInput(input){ + // While a game.capy is in a certain state, it will only react to a certain amount of inputs + if (this.game.capy.speedY > this.game.capy.gravity){ + this.game.capy.setState(states.FALLING, 1); + } else if (input.includes(' ') && this.game.energy > 0) { + this.game.capy.setState(states.CHARGING, 2) + } else if (input.includes('ArrowDown')) { + this.game.capy.setState(states.SLAMMING, 0) + } + } +} + +export class Falling extends State { // Child Class (sub class) + constructor(game){ + super('FALLING', game) + + } + enter(){ + this.game.capy.frameX = 0; + this.game.capy.maxFrame = 3.0; + this.game.capy.frameY = 9.5; + } + // Switch the game.capy into different states + handleInput(input){ + // While a game.capy is in a certain state, it will only react to a certain amount of inputs + if (this.game.capy.onGround()){ + this.game.capy.setState(states.WALKING, 1); + } else if (input.includes('ArrowDown')) { + this.game.capy.setState(states.SLAMMING, 0); + } + } +} + +export class Charging extends State { // Child Class (sub class) + constructor(game){ + super('CHARGING', game) + } + enter(){ + this.game.capy.frameX = 0; + this.game.capy.maxFrame = 3.0; + this.game.capy.frameY = 7.8; + } + // Switch the game.capy into different states + handleInput(input){ + this.energyDec = true; + if (this.game.energyInc >= 5) this.game.energyInc -= 5 + if (this.game.energyInc % 200 === 0) { + if (this.game.energy > 0) this.game.energy -= 1; + } + // .unshift() adds one or more elements to the beginning of an array & returns the new length of array + this.game.particles.unshift(new Fire(this.game, this.game.capy.x + this.game.capy.width * 0.5, this.game.capy.y + this.game.capy.height)); + // While a game.capy is in a certain state, it will only react to a certain amount of inputs + if (this.game.energy === 0) this.game.capy.setState(states.WALKING, 1) + if (!input.includes(' ') && this.game.capy.onGround()){ + this.game.capy.setState(states.WALKING, 1); + } else if (!input.includes(' ') && !this.game.capy.onGround()){ + this.game.capy.setState(states.FALLING, 1); + } else if (input.includes(' ') && input.includes('ArrowUp') && this.game.capy.onGround()) { + this.game.capy.speedY -= 27 + } else if (input.includes('ArrowDown')) { + this.game.capy.setState(states.SLAMMING, 0) + } + } +} + +export class Slamming extends State { // Child Class (sub class) + constructor(game){ + super('SLAMMING', game) + + } + enter(){ + this.game.capy.frameX = 0; + this.game.capy.maxFrame = 3.0; + this.game.capy.frameY = 12.5; + this.game.capy.speedY = 15; + } + // Switch the game.capy into different states + handleInput(input){ + // .unshift() adds one or more elements to the beginning of an array & returns the new length of array + this.game.particles.unshift(new Gravity(this.game, this.game.capy.x + this.game.capy.width * 0.5, this.game.capy.y + this.game.capy.height)); + // While a game.capy is in a certain state, it will only react to a certain amount of inputs + if (this.game.capy.onGround()){ + this.game.capy.setState(states.WALKING, 1); + for (let i = 0; i < 30; i++) + this.game.particles.unshift(new AOE(this.game, this.game.capy.x + this.game.capy.width * 0.5, this.game.capy.y + this.game.capy.height)); + this.game.capy.y = 427.5; + } else if (input.includes(' ') && !this.game.capy.onGround() && this.game.energy > 0){ + this.game.capy.setState(states.CHARGING, 2); + } + } +} + +export class Hurt extends State { // Child Class (sub class) + constructor(game){ + super('HURT', game) + + } + enter(){ + this.game.capy.frameX = 0; + this.game.capy.maxFrame = 7.0; + this.game.capy.frameY = 9.375; + } + // Switch the game.capy into different states + handleInput(input){ + // .unshift() adds one or more elements to the beginning of an array & returns the new length of array + // While a game.capy is in a certain state, it will only react to a certain amount of inputs + if (this.game.capy.frameX >= 7 && this.game.capy.onGround()){ + this.game.capy.setState(states.WALKING, 1); + } else if (this.game.capy.frameX >= 7 && !this.game.capy.onGround){ + this.game.capy.setState(states.FALLING, 1); + } + } +} \ No newline at end of file diff --git a/src/lib/components/error-game/js/collision.js b/src/lib/components/error-game/js/collision.js new file mode 100644 index 000000000..7911778c7 --- /dev/null +++ b/src/lib/components/error-game/js/collision.js @@ -0,0 +1,36 @@ +import { images } from './shared.js'; + +export class Boom { + constructor(game, x, y){ + this.game = game; + this.image = images.boomEffect; + this.boomWidth = 100; + this.boomHeight = 90; + this.sizeModifier = Math.random() + 0.5; + this.width = this.boomWidth * this.sizeModifier; + this.height = this.boomHeight * this.sizeModifier; + this.x = x - this.width * 0.5; + this.y = y - this.height * 0.5; + this.frameX = 0; + this.maxFrame = 4; + this.markedForDeletion = false; + this.fps = Math.random() * 10 + 5; + this.frameInterval = 1000/this.fps; + this.frameTimer = 0; + } + draw(context){ + context.drawImage(this.image, this.frameX * this.boomWidth, 0, this.boomWidth, this.boomHeight, this.x, this.y, this.width, this.height) + } + update(delta){ + this.x -= this.game.speed; + if (this.frameTimer > this.frameInterval){ + this.frameX++; + this.frameTimer = 0; + } else { + this.frameTimer += delta; + } + + if (this.frameX > this.maxFrame) this.markedForDeletion = true; + + } +} \ No newline at end of file diff --git a/src/lib/components/error-game/js/floatingText.js b/src/lib/components/error-game/js/floatingText.js new file mode 100644 index 000000000..835fea7ed --- /dev/null +++ b/src/lib/components/error-game/js/floatingText.js @@ -0,0 +1,24 @@ +export class FloatingText { + constructor(value, x, y, targetX, targetY) { + this.value = value; + this.x = x; + this.y = y; + this.targetX = targetX; + this.targetY = targetY; + this.markedForDeletion = false; + this.timer = 0; + } + update(){ + this.x += (this.targetX - this.x) * 0.03; + this.y += (this.targetY - this.y) * 0.03; + this.timer++; + if (this.timer > 100) this.markedForDeletion = true; + } + draw(context){ + context.font = '20px Dela Gothic One'; + context.fillStyle = 'white'; + context.fillText(this.value, this.x, this.y); + context.fillStyle = 'black'; + context.fillText(this.value, this.x - 2, this.y - 2); + } +} \ No newline at end of file diff --git a/src/lib/components/error-game/js/input.js b/src/lib/components/error-game/js/input.js new file mode 100644 index 000000000..ef923e3b7 --- /dev/null +++ b/src/lib/components/error-game/js/input.js @@ -0,0 +1,54 @@ +export class InputHandler { + constructor(game){ + this.game = game; + this.keys = []; + this.touchY = ''; + this.touchTreshold = 30; + window.addEventListener('keydown', e => { + // If key is arrowdown and the key that was pressed is not + // included in the this.keys array, push arrowdown into this.keys array + if ((e.key === "ArrowDown" || + e.key === "ArrowUp" || + e.key === "ArrowLeft" || + e.key === "ArrowRight" || + e.key === " " + ) && this.keys.indexOf(e.key) === -1){ + this.keys.push(e.key); + } else if (e.key === 'd') { + this.game.debug = !this.game.debug; + } else if (e.key === "Enter" && this.game.gameOver) { + this.game.restart(); + } + + }); + window.addEventListener('keyup', e => { + // If key that was released is arrowdown, Use splice method to + // remove it from this.keys array + if (e.key === 'ArrowDown' || + e.key ==='ArrowUp' || + e.key === "ArrowLeft" || + e.key === "ArrowRight" || + e.key === " "){ + this.keys.splice(this.keys.indexOf(e.key), 1) + } + // Whenever someone touches browser window + }); + window.addEventListener('touchstart', e => { + this.touchY = e.changedTouches[0].pageY; + + }); + window.addEventListener('touchmove', e => { + const swipeDistance = e.changedTouches[0].pageY - this.touchY + + if (swipeDistance < -this.touchThreshold && this.keys.indexOf('swipe up') === -1) this.keys.push('swipe up'); + else if (swipeDistance > this.touchTreshold && this.keys.indexOf('swipe down') === -1) { + this.keys.push('swipe down'); + if (this.game.gameOver) this.game.restart() + } + }); + window.addEventListener('touchend', e => { + this.keys.splice(this.keys.indexOf('swipe up'), 1); + this.keys.splice(this.keys.indexOf('swipe down'), 1); + }); + } +} \ No newline at end of file diff --git a/src/lib/components/error-game/js/mobs.js b/src/lib/components/error-game/js/mobs.js new file mode 100644 index 000000000..7643f6533 --- /dev/null +++ b/src/lib/components/error-game/js/mobs.js @@ -0,0 +1,179 @@ +import { images,isMobileDevice } from './shared.js'; + +class Mob { + constructor(){ + this.frameX = 0; + this.frameY = 0; + this.fps = 20; + this.frameInterval = 1000/this.fps; + this.frameTimer = 0; + this.markedForDeletion = false; + } + update(delta){ + // movement + this.x -= this.speedX + this.game.speed; + this.y += this.speedY; + if (this.frameTimer > this.frameInterval){ + this.frameTimer = 0; + if (this.frameX < this.maxFrame) this.frameX++; + else this.frameX = 0; + } else { + this.frameTimer += delta; + } + // Check if Mob is off screen + if (this.x + this.width < 0) this.markedForDeletion = true; + } + draw(context){ + context.strokeStyle = 'rgba(0, 0, 0, 0)'; + if (this.game.debug) context.strokeRect(this.x, this.y, this.width, this. height) + context.drawImage(this.image, this.frameX * this.width, 0, this.width, this.height, this.x, this.y, this.width, this.height) + } +} + +export class FlyingMob extends Mob { + constructor(game){ + super(); // runs code from parent class + this.name = "bee"; + this.game = game; + this.width = 25; + this.height = 26; + this.x = this.game.width + this.y = Math.random() * this.game.height * 0.5 + this.game.hudHeight; + this.speedX = Math.random() + 1; + this.speedY = 0; + this.maxFrame = 2.5; + this.image = images.beeImage; + // Move the flying enemies up and down as they move + this.angle = 0; + this.angleValue = Math.random() * 0.1 + 0.1; + + } + update(delta){ + super.update(delta); + this.angle += this.angleValue + this.y += Math.sin(this.angle) // Maps positions of flying mobs along sin waves + } + +} + +export class GroundMob { + constructor(game){ + this.name = "wolf"; + this.frameX = 0; + this.frameY = 0; + this.game = game; + this.width = 84.6; + this.height = 51; + this.x = this.game.width; + this.y = this.game.height - this.height - this.game.groundMargin; + this.image = images.wolfImage; + this.speedX = isMobileDevice() ? -0.1: -0.5; + this.speedY = 0; + this.maxFrame = 4; + this.fps = 20; + this.frameInterval = 1000/this.fps; + this.frameTimer = 0; + this.markedForDeletion = false; + } + update(delta) { + this.x -= this.speedX + this.game.speed; + this.y += this.speedY; + if (this.frameTimer > this.frameInterval){ + this.frameTimer = 0; + if (this.frameX > 0){ + this.frameX--; + } + else this.frameX = this.maxFrame; + } else { + this.frameTimer += delta; + } + // Check if Mob is off screen + if (this.x + this.width < 0) this.markedForDeletion = true; + } + + draw(context){ + context.strokeStyle = 'rgba(0, 0, 0, 0)'; + if (this.game.debug) context.strokeRect(this.x, this.y, this.width, this. height) + context.drawImage(this.image, this.frameX * this.width, 0, this.width, this.height, this.x, this.y, this.width, this.height) + } +} + +export class Hedgehog extends Mob { + constructor(game){ + super(); + this.name = "hedgehog"; + this.game = game; + this.width = 24; + this.height = 25; + this.x = this.game.width; + this.y = this.game.height - this.height - this.game.groundMargin; + this.image = images.hedgehogImage; + this.speedX = -1.5 + this.speedY = 0; + this.maxFrame = 5; + this.fps = 20; + this.frameInterval = 1000/this.fps; + this.frameTimer = 0; + this.markedForDeletion = false; + } + update(delta) { + this.x -= this.speedX + this.game.speed; + this.y += this.speedY; + if (this.frameTimer > this.frameInterval){ + this.frameTimer = 0; + if (this.frameX > 0) this.frameX--; + else this.frameX = this.maxFrame; + } else { + this.frameTimer += delta; + } + + // Check if Mob is off screen + if (this.x + this.width < 0) this.markedForDeletion = true; + } + draw(context) { + context.strokeStyle = 'rgba(0, 0, 0, 0)'; + if (this.game.debug) context.strokeRect(this.x, this.y, this.width, this. height) + context.drawImage(this.image, this.frameX * this.width, 0, this.width, this.height, this.x, this.y, this.width, this.height) + } +} +export class Wizard extends Mob{ + constructor(game){ + super(); + this.name = "wizard"; + this.width = 80; + this.height = 80; + this.game = game; + this.frameX = 0; + this.frameY = 0; + this.x = this.game.width; + this.y = Math.random() * this.game.height * 0.5 + this.game.hudHeight; + this.speedX = 0.3; + this.speedY = 0; + this.maxFrame = 9; + this.image = images.wizardImage; + + } + update(delta){ + // movement + this.x -= this.speedX + this.game.speed; + this.y += this.speedY; + if (this.frameTimer > this.frameInterval){ + this.frameTimer = 0; + if (this.frameX < this.maxFrame) this.frameX++; + else this.frameX = 0; + } else { + this.frameTimer += delta; + } + // Check if Mob is off screen + if (this.x + this.width < 0) this.markedForDeletion = true; + } + draw(context){ + context.strokeStyle = 'rgba(0, 0, 0, 0)'; + if (this.game.debug) context.strokeRect(this.x, this.y, this.width, this. height) + context.drawImage(this.image, this.frameX * this.width, 0, this.width, this.height, this.x, this.y, this.width, this.height) + } +} + + + + diff --git a/src/lib/components/error-game/js/particles.js b/src/lib/components/error-game/js/particles.js new file mode 100644 index 000000000..843f45df5 --- /dev/null +++ b/src/lib/components/error-game/js/particles.js @@ -0,0 +1,102 @@ +import { images } from "./shared.js"; + +class Particle { + constructor(game){ + this.game = game; + this.markedForDeletion = false; + } + update(){ + this.x -= this.speedX + this.game.speed; + this.y -= this.speedY; + this.size *= 0.98; + if (this.size < 0.5) this.markedForDeletion = true; + } +} + +export class Dust extends Particle { + constructor(game, x, y){ + super(game); + this.size = Math.random() * 10 + 10; + this.x = x; + this.y = y; + this.speedX = Math.random(); + this.speedY = Math.random(); + this.color = 'rgba(0, 0, 0, 0.15)' + } + draw(context){ + context.beginPath(); + context.arc(this.x + 5, this.y, this.size, 0, Math.PI * 2); + context.fillStyle = this.color + context.fill() + } +} + +export class Fire extends Particle { + constructor(game, x, y){ + super(game); + this.image = images.fireEffect; + this.size = Math.random() * 100 + 50 + this.x = x; + this.y = y; + this.speedX = 1; + this.speedY = 1; + this.va = Math.random() * 0.2 - 0.1 + } + update(){ + super.update(); + this.angle += this.va + } + draw(context){ + context.save(); + context.translate(this.x, this.y - 105); + context.rotate(this.angle); + context.drawImage(this.image, 0, 0, this.size, this.size); + context.restore(); + } +} + +export class Gravity extends Particle { + constructor(game, x, y){ + super(game); + this.image = images.gravityEffect; + this.size = Math.random() * 100 + 50 + this.x = x; + this.y = y; + this.speedX = 1; + this.speedY = 1; + this.va = Math.random() * 0.2 - 0.1 + } + update(){ + super.update(); + this.angle += this.va + + } + draw(context){ + context.save(); + context.translate(this.x, this.y - 105); + context.rotate(this.angle); + context.drawImage(this.image, 0, 0, this.size, this.size); + context.restore(); + } +} + +export class AOE extends Particle { + constructor(game, x, y){ + super(game); + this.size = Math.random() * 100 + 100; + this.x = x - this.size * 0.4; + this.y = y - this.size * 0.4; + this.speedX = Math.random() * 6 - 4; + this.speedY = Math.random() * 2 + 1; + this.gravity = 0; + this.image = images.gravityEffect; + } + update(){ + super.update(); + this.gravity + 0.1; + this.y += this.gravity; + } + draw(context){ + context.drawImage(this.image, this.x, this.y, this.size, this.size) + } +} diff --git a/src/lib/components/error-game/js/shared.js b/src/lib/components/error-game/js/shared.js new file mode 100644 index 000000000..89c04db6d --- /dev/null +++ b/src/lib/components/error-game/js/shared.js @@ -0,0 +1,34 @@ +const imageArr = [ + 'capyImage.png', + 'layer1.png', + 'layer2.png', + 'layer3.png', + 'layer4.png', + 'beeImage.png', + 'wizardImage.png', + 'wolfImage.png', + 'hedgehogImage.png', + 'fireEffect.gif', + 'gravityEffect.gif', + 'boomEffect.png', + 'bars.png', +]; + +/** + * @type {Object.} + */ +const images = {}; + +export function loadImages() { + imageArr.forEach((imageName) => { + const name = imageName.split('.')[0]; + images[name] = new Image(); + images[name].src = '/assets/error-game/' + imageName; + }); +} + +export { images }; + +export function isMobileDevice() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); +} diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte index 84b3704ba..593b8ba6c 100644 --- a/src/routes/+error.svelte +++ b/src/routes/+error.svelte @@ -1,77 +1,58 @@ + import { onMount, onDestroy } from 'svelte'; + import { AcmTheme, theme } from '$lib/public/legacy/theme'; + import ErrorGame from '$lib/components/error-game/game.svelte'; - + function changeTheme(event: MediaQueryListEvent) { + theme.set(event.matches ? AcmTheme.Dark : AcmTheme.Light); + } - - ACM at CSUF / {$page.status || 404} - + function preventKeyScroll(event: KeyboardEvent) { + // Prevent spacebar and down arrow from scrolling (interferes with game) + if (event.code === 'Space' || event.code === 'ArrowDown') { + event.preventDefault(); + } + } - - - - - 404 - Can't find where you're going? - Head home! - - - - - - + onMount(() => { + theme.init(); - + const mediaList = window.matchMedia('(prefers-color-scheme: dark)'); + mediaList.addEventListener('change', changeTheme); - diff --git a/static/assets/error-game/bars.png b/static/assets/error-game/bars.png new file mode 100644 index 000000000..03d7eba55 Binary files /dev/null and b/static/assets/error-game/bars.png differ diff --git a/static/assets/error-game/batImage.png b/static/assets/error-game/batImage.png new file mode 100644 index 000000000..455a436d0 Binary files /dev/null and b/static/assets/error-game/batImage.png differ diff --git a/static/assets/error-game/beeImage.png b/static/assets/error-game/beeImage.png new file mode 100644 index 000000000..4bef778d4 Binary files /dev/null and b/static/assets/error-game/beeImage.png differ diff --git a/static/assets/error-game/boomEffect.png b/static/assets/error-game/boomEffect.png new file mode 100644 index 000000000..377b52d43 Binary files /dev/null and b/static/assets/error-game/boomEffect.png differ diff --git a/static/assets/error-game/capyImage.png b/static/assets/error-game/capyImage.png new file mode 100644 index 000000000..a9859b318 Binary files /dev/null and b/static/assets/error-game/capyImage.png differ diff --git a/static/assets/error-game/fireEffect.gif b/static/assets/error-game/fireEffect.gif new file mode 100644 index 000000000..4f3f1bc92 Binary files /dev/null and b/static/assets/error-game/fireEffect.gif differ diff --git a/static/assets/error-game/gravityEffect.gif b/static/assets/error-game/gravityEffect.gif new file mode 100644 index 000000000..6cdcbeec9 Binary files /dev/null and b/static/assets/error-game/gravityEffect.gif differ diff --git a/static/assets/error-game/hedgehogImage.png b/static/assets/error-game/hedgehogImage.png new file mode 100644 index 000000000..1ed8b77b3 Binary files /dev/null and b/static/assets/error-game/hedgehogImage.png differ diff --git a/static/assets/error-game/layer1.png b/static/assets/error-game/layer1.png new file mode 100644 index 000000000..9eed2cfd8 Binary files /dev/null and b/static/assets/error-game/layer1.png differ diff --git a/static/assets/error-game/layer2.png b/static/assets/error-game/layer2.png new file mode 100644 index 000000000..795d9b095 Binary files /dev/null and b/static/assets/error-game/layer2.png differ diff --git a/static/assets/error-game/layer3.png b/static/assets/error-game/layer3.png new file mode 100644 index 000000000..f014539a5 Binary files /dev/null and b/static/assets/error-game/layer3.png differ diff --git a/static/assets/error-game/layer4.png b/static/assets/error-game/layer4.png new file mode 100644 index 000000000..ba941187a Binary files /dev/null and b/static/assets/error-game/layer4.png differ diff --git a/static/assets/error-game/wizardImage.png b/static/assets/error-game/wizardImage.png new file mode 100644 index 000000000..755cb3db5 Binary files /dev/null and b/static/assets/error-game/wizardImage.png differ diff --git a/static/assets/error-game/wolfImage.png b/static/assets/error-game/wolfImage.png new file mode 100644 index 000000000..65ff344f4 Binary files /dev/null and b/static/assets/error-game/wolfImage.png differ