diff --git a/.git-branches.toml b/.git-branches.toml new file mode 100644 index 0000000..4a4a0b0 --- /dev/null +++ b/.git-branches.toml @@ -0,0 +1,103 @@ +# Git Town configuration file +# +# Run "git town config setup" to add additional entries +# to this file after updating Git Town. +# +# The "push-hook" setting determines whether Git Town +# permits or prevents Git hooks while pushing branches. +# Hooks are enabled by default. If your Git hooks are slow, +# you can disable them to speed up branch syncing. +# +# When disabled, Git Town pushes using the "--no-verify" switch. +# More info at https://www.git-town.com/preferences/push-hook. +push-hook = true + +# Should Git Town push the new branches it creates +# immediately to origin even if they are empty? +# +# When enabled, you can run "git push" right away +# but creating new branches is slower and +# it triggers an unnecessary CI run on the empty branch. +# +# When disabled, many Git Town commands execute faster +# and Git Town will create the missing tracking branch +# on the first run of "git sync". +push-new-branches = true + +# Should "git ship" delete the tracking branch? +# You want to disable this if your code hosting platform +# (GitHub, GitLab, etc) deletes head branches when +# merging pull requests through its UI. +ship-delete-tracking-branch = false + +# Should "git ship" sync branches before shipping them? +# +# Guidance: enable when shipping branches locally on your machine +# and disable when shipping feature branches via the code hosting +# API or web UI. +# +# When enabled, branches are always fully up to date when shipped +# and you get a chance to resolve merge conflicts +# between the feature branch to ship and the main development branch +# on the feature branch. This helps keep the main branch green. +# But this also triggers another CI run and delays shipping. +sync-before-ship = false + +# Should "git sync" also fetch updates from the upstream remote? +# +# If an "upstream" remote exists, and this setting is enabled, +# "git sync" will also update the local main branch +# with commits from the main branch at the upstream remote. +# +# This is useful if the repository you work on is a fork, +# and you want to keep it in sync with the repo it was forked from. +sync-upstream = false + +[branches] + +# The main branch is the branch from which you cut new feature branches, +# and into which you ship feature branches when they are done. +# This branch is often called "main", "master", or "development". +main = "main" + +# Perennial branches are long-lived branches. +# They are never shipped and have no ancestors. +# Typically, perennial branches have names like +# "development", "staging", "qa", "production", etc. +# +# See also the "perennial-regex" setting. +perennials = ["release"] + +# All branches whose names match this regular expression +# are also considered perennial branches. +# +# If you are not sure, leave this empty. +perennial-regex = "" + +[hosting] + +# Knowing the type of code hosting platform allows Git Town +# to open browser URLs and talk to the code hosting API. +# Most people can leave this on "auto-detect". +# Only change this if your code hosting server uses as custom URL. +platform = "github" + +# When using SSH identities, define the hostname +# of your source code repository. Only change this +# if the auto-detection does not work for you. +# origin-hostname = "" + +[sync-strategy] + +# How should Git Town synchronize feature branches? +# Feature branches are short-lived branches cut from +# the main branch and shipped back into the main branch. +# Typically you develop features and bug fixes on them, +# hence their name. +feature-branches = "merge" + +# How should Git Town synchronize perennial branches? +# Perennial branches have no parent branch. +# The only updates they receive are additional commits +# made to their tracking branch somewhere else. +perennial-branches = "merge" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/assets/background.png b/assets/background.png new file mode 100644 index 0000000..d92dc12 Binary files /dev/null and b/assets/background.png differ diff --git a/script.js b/script.js index cdaaadf..e990f1a 100644 --- a/script.js +++ b/script.js @@ -1,21 +1,29 @@ import StateManager from './src/stateManager.js' +// once all the HTML, CSS and other assets have loaded, +// we start with the JS init. window.addEventListener("load", function() { + + // get reference to the canvas object defined in the html page const canvas = this.document.getElementById('canvas1'); const ctx = canvas.getContext("2d"); canvas.width = 600; canvas.height = 800; + ctx.fillStyle = "white"; + ctx.strokeStyle = "white"; + ctx.lineWidth = 5; + ctx.font = "20px Impact"; const stateManager = new StateManager(canvas); - - // clear rect and paint the whole scene again function animate() { + // clear rect and paint the whole scene again ctx.clearRect(0, 0, canvas.width, canvas.height) stateManager.render(ctx); + // recursively calls this function to re-draw the updated components window.requestAnimationFrame(animate); } + // start the recursive loop for animation animate(); -} -) +}) diff --git a/src/ammo.js b/src/ammo.js index 587079e..0db9a5a 100644 --- a/src/ammo.js +++ b/src/ammo.js @@ -4,7 +4,7 @@ class Ammo { this.height = 20; this.x = 0; this.y = 0; - this.speed = 20; + this.speed = 30; this.free = true; this.stateManager = stateManager; } diff --git a/src/enemy.js b/src/enemy.js new file mode 100644 index 0000000..a6f466e --- /dev/null +++ b/src/enemy.js @@ -0,0 +1,55 @@ +class Enemy { + + constructor(stateManager, offsetInRaidX, offsetInRaidY) { + this.stateManager = stateManager; + this.enemyPosX = 0; + this.enemyPosY = 0; + + // enemy hits and health + this.hitCounter = 0; + this.healthPoints = 1; + + // the raid moves in a grid, the offset defines where the enemy is in the raid relative + // to the raid's x and y position + this.offsetInRaidX = offsetInRaidX; + this.offsetInRaidY = offsetInRaidY; + } + + render(context) { + context.strokeRect(this.enemyPosX, this.enemyPosY, this.stateManager.enemySize, this.stateManager.enemySize); + } + + // as the raid moves, add the offset to the enemy position and move it to + progress(raidPosX, raidPosY) { + this.enemyPosX = raidPosX + this.offsetInRaidX; + this.enemyPosY = raidPosY + this.offsetInRaidY; + + // check if an enemy has crossed the lower bounds of the game + // if yes, finish the game + if (this.enemyPosY + this.stateManager.enemySize > this.stateManager.height) { + this.stateManager.gameOver = true; + } + + // if the game is still going, check for enemy collisions active ammunitions + if (!this.stateManager.gameOver) { + // check for collisions + this.stateManager.availableAmmoPool.forEach(ammo => { + if (!ammo.free && this.stateManager.checkEnemyCollision(this, ammo)) { + this.hitCounter++; + ammo.reset(); + if (!this.stateManager.gameOver) + this.stateManager.score++; + } + }) + + } + + // check if enemy hit the player + if (this.stateManager.checkEnemyCollision(this, this.stateManager.player)) { + this.stateManager.player.lives--; + this.hitCounter++; + } + } +} + +export default Enemy; diff --git a/src/player.js b/src/player.js index 445267e..f1f3e7b 100644 --- a/src/player.js +++ b/src/player.js @@ -4,15 +4,24 @@ class Player { this.stateManager = stateManager; this.width = 100; this.height = 100; + this.speed = 5; + + // variables + this.lives = 3; // starting player with 3 lives this.x = this.stateManager.width * 0.5 - this.width * 0.5; this.y = this.stateManager.height - this.height; - this.speed = 5; } render(context) { context.fillRect(this.x, this.y, this.width, this.height); } + reset() { + this.lives = 3; // starting player with 3 lives + this.x = this.stateManager.width * 0.5 - this.width * 0.5; + this.y = this.stateManager.height - this.height; + } + update() { // Handle intent to move left if (this.stateManager.activeKeys.includes('ArrowLeft') diff --git a/src/raid.js b/src/raid.js new file mode 100644 index 0000000..da34eca --- /dev/null +++ b/src/raid.js @@ -0,0 +1,62 @@ +import Enemy from './enemy.js'; + +class Raid { + constructor(stateManager) { + this.stateManager = stateManager; + + this.height = this.stateManager.enemyRaidGridRows * this.stateManager.enemySize; + this.width = this.stateManager.enemyRaidGridColumns * this.stateManager.enemySize; + + this.raidPosX = 0; + // start the raid above the screen + this.raidPosY = -this.height; + + this.speedX = 3; + this.speedY = 0; + + this.enemies = []; + this.createEnemyRaid(); + this.destroyed = false; + } + + createEnemyRaid() { + for (let y = 0; y < this.stateManager.enemyRaidGridRows; y++) { + for (let x = 0; x < this.stateManager.enemyRaidGridColumns; x++) { + this.enemies.push(new Enemy(this.stateManager, x * this.stateManager.enemySize, y * this.stateManager.enemySize)); + } + } + } + + render(context) { + if (this.enemies.length <= 0) { + return; + } + if (this.raidPosY < 0) { + // if the raid has just spawned above the screen, float it down + this.speedY = 5; + } else { + // by default keep the vertical speed as 0 and update only when the horizontal limit is touched + this.speedY = 0; + } + + // horizontal boundary check, if true, reverse the horizontal speed and increase the vertical speed by the height of the enemy + if (this.raidPosX > this.stateManager.width - this.width || this.raidPosX < 0) { + this.speedX *= -1; + this.speedY = this.stateManager.enemySize; + } + + // udpate position of the wave + this.raidPosX += this.speedX; + this.raidPosY += this.speedY; + + // update the position of the enemies in the wave relative to the wave position + this.enemies.forEach(enemy => { + enemy.progress(this.raidPosX, this.raidPosY); + enemy.render(context); + }) + + // check if the enemy is shot + this.enemies = this.enemies.filter(enemy => enemy.hitCounter < enemy.healthPoints); + } +} +export default Raid; diff --git a/src/stateManager.js b/src/stateManager.js index 78a739d..5990630 100644 --- a/src/stateManager.js +++ b/src/stateManager.js @@ -1,5 +1,6 @@ import Player from './player.js'; import Ammo from './ammo.js'; +import Raid from './raid.js'; class StateManager { constructor(canvas) { @@ -8,16 +9,34 @@ class StateManager { this.height = this.canvas.height; this.player = new Player(this); + // game state variables + this.score = 0; + this.gameOver = false; + + // Ammunition pool this.availableAmmoPool = []; this.maxAmmo = 15; - this.createAmmoPool(); - this.activeKeys = []; + // Enemy wave control + this.enemySize = 60; + this.enemyRaidGridColumns = 2; + this.enemyRaidGridRows = 2; + this.raids = []; + this.raidCount = 1; + this.newRaidSpawned = false; + + + // init functions + this.startEnemyRaid(); + this.createAmmoPool(); // keyboard event listeners + this.activeKeys = []; - // when key is pushed down, it is added to active key list - // we also need to make sure no duplicate keys are present + // when key is pushed down and it is not already present in the active key list + // it is added to active key list + // This is done because if you hold down the key, then it will keep firing the + // same keydown event for the key which we don't want window.addEventListener('keydown', (e) => { if (!this.activeKeys.includes(e.key)) { this.activeKeys.push(e.key); @@ -27,6 +46,11 @@ class StateManager { if (e.key === 'w') { this.player.fireAmmo(); } + + // if user presses r and the game is over, restart the game + if (e.key === 'r' || this.gameOver) { + this.restartGame(); + } }) // remove key from the active key list when key is no longer pressed @@ -35,6 +59,21 @@ class StateManager { }) } + restartGame() { + this.player.reset(); + this.score = 0; + this.gameOver = false; + this.availableAmmoPool = []; + this.raids = []; + this.raidCount = 1; + this.enemyRaidGridColumns = 2; + this.enemyRaidGridRows = 2; + this.newRaidSpawned = false; + + this.startEnemyRaid(); + this.createAmmoPool(); + } + createAmmoPool() { for (let index = 0; index < this.maxAmmo; index++) { this.availableAmmoPool.push(new Ammo(this)); @@ -45,13 +84,81 @@ class StateManager { return this.availableAmmoPool.find((ammo) => ammo.free); } + startEnemyRaid() { + this.raids.push(new Raid(this)); + } + + createNewRaid() { + this.newRaidSpawned = true; + if (Math.random() < 0.5) { + this.enemyRaidGridColumns++; + } else { + this.enemyRaidGridRows++; + } + this.startEnemyRaid(); + this.raidCount++; + + // there's a small chance of player life increasing after wave finish + if (Math.random() < 0.1) { + this.player.lives++; + } + this.newRaidSpawned = false; + } + + // check if ammo has hit enemy or enemy has hit player + checkEnemyCollision(enemy, object) { + // using the 2d collision check for this + // https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection + return (object.x < enemy.enemyPosX + this.enemySize && + object.x + object.width > enemy.enemyPosX && + object.y < enemy.enemyPosY + this.enemySize && + object.y * object.height > enemy.enemyPosY + ); + } + render(context) { this.player.render(context); + this.player.update(); this.availableAmmoPool.forEach(ammo => { ammo.progress() ammo.render(context) }) + this.raids.forEach(raid => { + if (raid.destroyed) { + return; + } + raid.render(context); + + if (raid.enemies.length < 1 && !this.newRaidSpawned && !this.gameOver) { + raid.destroyed = true; + this.createNewRaid(); + } + }) + + // check for player's lives + if (this.player.lives <= 0) { + this.gameOver = true; + this.player.lives = 0; + } + + // show game stats + context.save(); + context.fillText("Score: " + this.score, 10, 30); + context.fillText("Raid: " + this.raidCount, 10, 60); + context.fillText("Lives: ", 10, 90); + for (let i = 0; i < this.player.lives; i++) { + context.fillRect(53 + 10 * (i + 1), 75, 5, 15) + } + + if (this.gameOver) { + context.textAlign = "center"; + context.font = "100px Impact"; + context.fillText("Game Over", this.width * 0.5, this.height * 0.5); + context.font = "30px Impact"; + context.fillText("Press R to restart", this.width * 0.5, this.height * 0.5 + 40); + } + context.restore(); } } diff --git a/style.css b/style.css index 1fefb02..bfdc010 100644 --- a/style.css +++ b/style.css @@ -4,4 +4,5 @@ top: 50%; left: 50%; translate: -50% -50%; + background: url("assets/background.png"); }