diff --git a/2024-01-08-CSSE-oop-game-levels2.md b/2024-01-08-CSSE-oop-game-levels2.md
new file mode 100644
index 0000000..2813cf4
--- /dev/null
+++ b/2024-01-08-CSSE-oop-game-levels2.md
@@ -0,0 +1,107 @@
+---
+layout: base
+title: Dynamic Game Levels v2.0
+description: Incorporate student lessons goombas, platforms, parallax backgrounds, settings with local storage, etc. Refactor code introducing GameSetup and SettingsControl. Style is moved into _sass.
+type: collab
+courses: { csse: {week: 18} }
+image: /images/platformer/backgrounds/hills.png
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/_config.yml b/_config.yml
index 2b0791c..cffdef0 100644
--- a/_config.yml
+++ b/_config.yml
@@ -19,5 +19,5 @@ plugins:
- jekyll-remote-theme
future: true
header_pages:
- - 2023-11-27-CSSE-oop-game-levels.md
+ - 2024-01-08-CSSE-oop-game-levels2.md
- AC_csse.md
\ No newline at end of file
diff --git a/_sass/minima/custom-styles.scss b/_sass/minima/custom-styles.scss
index 0d0abbf..5f34427 100644
--- a/_sass/minima/custom-styles.scss
+++ b/_sass/minima/custom-styles.scss
@@ -24,4 +24,7 @@
// @import "minima/hamilton/skins/daylight";
// @import "minima/hamilton/skins/midnight";
// @import "minima/hamilton/skins/sunrise";
-// @import "minima/hamilton/skins/sunset"
\ No newline at end of file
+// @import "minima/hamilton/skins/sunset";
+
+// FOR PLATFORMER
+@import "minima/platformer-styles.scss"
\ No newline at end of file
diff --git a/_sass/minima/platformer-styles.scss b/_sass/minima/platformer-styles.scss
new file mode 100644
index 0000000..b343649
--- /dev/null
+++ b/_sass/minima/platformer-styles.scss
@@ -0,0 +1,123 @@
+/// platformer-styles.scss
+/// @description This stylesheet is used to style the platformer game.
+
+/// Coding Syles in SCCS (Sass), from code below:
+/// @mixin is a block of CSS declarations that can be reused in other classes, see @mixin dark-mode-color below.
+/// @include is used to include a mixin in a class, see @include dark-mode-color below.
+/// @extend is used to extend a class with another class, see %button below.
+/// $variable is used to define a variable, see $spacing-unit, $high-emph, $dark-grey below.
+/// #id is used to define an id, see #gameBegin, #gameOver, #settings below.
+/// .class is used to define a class, see .sidebar below.
+/// & is used to reference the parent selector, see &:hover below.
+/// the double slash comments (//) are considered regular comments in Sass.
+/// the triple slash comments (///) are considered documentation comments in Sass.
+/// comments are not included in the compiled CSS file.
+/// @import is used to import other stylesheets, see @import of this file in "_sass/minima/custom-styles.sccs".
+/// see https://sass-lang.com/documentation/syntax for more information.
+
+/// Score for the game
+/// @description This ID is used to style the score for the game, key style is to ensure it is on top of other elements.
+#score {
+ position: relative;
+ z-index: 2; // z-index position ensures these buttons are on top.
+ padding: 5px;
+ color: $dt-red !important; // use the high-emphasis color for the text
+ }
+
+ /// Game Begin, Game Over, Settings IDs
+ /// @description These IDs correspond to buttons in game screen. Key styling is to ensure they are on top of other elements.
+ #gameBegin, #gameOver, #settings, #leaderboard {
+ position: relative;
+ z-index: 2; // z-index position ensures these buttons are on top.
+ }
+
+ /// Color mixin
+ /// @description This mixin is defined to share colors with classes in this stylesheet.
+ @mixin dark-mode-color {
+ color: $high-emph !important; // use the high-emphasis color for the text
+ background-color: $dark-grey !important; // use the dark grey color for the background
+ border: 1px solid $dt-blue; // from dracula-highlight.scss
+ }
+
+ /// Horizontal Submenu
+ /// @description This class styles the horizontal submenu for score and buttons in the game. It has many style elements (e.g. placing submenu below header).
+ .submenu {
+ @include dark-mode-color; // use the dark mode color scheme
+ border: none; // remove setting as submenu
+ position: fixed;
+ z-index: 3; // Position on Top of other elements, as well as ID buttons above
+ top: $spacing-unit * 1.865; // matches minima ".site-header min-height height", reference: https://github.com/jekyll/minima/blob/master/_sass/minima/_layout.scss
+
+ // Styles for the score and buttons
+ #score, #gameBegin, #gameOver, #settings, #leaderboard {
+ display: inline-block; // Make the score and buttons inline
+ margin-right: $spacing-unit; // Add some margin to the right of each item
+ }
+ }
+
+ /// Sidebar class
+ /// @description This class styles the sidebar for settings in the game. It has many style elements (e.g. placing sidebar below header).
+ .sidebar {
+ @include dark-mode-color; // use the dark mode color scheme
+ border: none; // remove setting as sidebar collapse leaves residue line
+ position: fixed;
+ z-index: 3; // Position on Top of other elements, as well as ID buttons above
+ // left: 0; // set the sidebar to be on the left side of the page
+ top: 0; // calculated to be below the header and submenu
+ padding-top: 5px;
+ padding-bottom: 5px;
+ overflow-x: hidden; /* Disable horizontal scroll */
+ transition: 0.5s; /* 0.5-second transition effect to slide in the sidebar */
+ // following are changed by JavaScript
+ width: 0px;
+ padding-left: 0px;
+ padding-right: 0px;
+ }
+
+ /// Table class
+ /// @description Key style is to present game levels and make it responsive with hover highlighting.
+ .table.levels {
+ tr {
+ cursor: pointer; // Change the cursor to a pointer when it hovers over a row
+ td {
+ border: 1px solid $dt-blue; // from dracula-highlight.scss
+ }
+ &:hover { // Change the background when a row is hovered over
+ background-color: $dt-blue; // from dracula-highlight.scss
+
+ td {
+ background-color: inherit; // Make the background color of the td elements the same as the tr element
+ }
+ }
+ }
+ }
+
+ /// Input mixin
+ /// @description This mixin sets some common styles (e.g. color, height) for the input fields, avoiding duplication of common styles in each input class.
+ /// @param {Number} width - The width of the input field, how wide it is.
+ /// @param {Number} height - The height of the input field, how tall it is.
+ /// @param {Color} border - The border color of the input field, the outline color.
+ /// @param {Number} border-radius - The border radius of the input field, rounded corners.
+ /// @param {Number} padding - The padding inside the input field, space between the border and the text.
+ /// @param {String} text-align - The text alignment inside the input field, left, right, or center.
+ @mixin input {
+ @include dark-mode-color; // use the dark mode color scheme
+ height: 30px;
+ border-radius: 5px;
+ padding: 5px;
+ }
+
+ /// Input class
+ /// @description This class styles the userID input field, key style is to make it wider.
+ .input.userID {
+ @include input;
+ width: 100px; // customize width of the input field
+ }
+
+ /// Input class
+ /// @description This class styles the gameSpeed and gravity input fields, key style is to right-align the text.
+ .input.gameSpeed, .input.gravity {
+ @include input;
+ width: 40px; // customize width of the input field
+ text-align: right; // right-align input for numbers
+ }
\ No newline at end of file
diff --git a/assets/js/platformer2/Background.js b/assets/js/platformer2/Background.js
new file mode 100644
index 0000000..0315559
--- /dev/null
+++ b/assets/js/platformer2/Background.js
@@ -0,0 +1,54 @@
+import GameEnv from './GameEnv.js';
+import GameObject from './GameObject.js';
+
+export class Background extends GameObject {
+ constructor(canvas, image, data) {
+ super(canvas, image, data);
+ }
+
+ /* Update uses modulo math to cycle to start at width extent
+ * x is position in cycle
+ * speed can be used to scroll faster
+ * width is extent of background image
+ */
+ update() {
+ this.x = (this.x - this.speed) % this.width;
+ }
+
+ /* To draws are used to capture primary frame and wrap around ot next frame
+ * x to y is primary draw
+ * x + width to y is wrap around draw
+ */
+ draw() {
+ this.ctx.drawImage(this.image, this.x, this.y);
+ this.ctx.drawImage(this.image, this.x + this.width, this.y);
+ }
+
+ /* Background camvas is set to screen
+ * the ADJUST contant elements portions of image that don't wrap well
+ * the GameEnv.top is a getter used to set canvas under Menu
+ * the GameEnv.bottom is setter used to establish game bottom at offsetHeight of canvas
+ */
+ size() {
+ // Update canvas size
+ const ADJUST = 1 // visual layer adjust; alien_planet.jpg: 1.42, try 1 for others
+
+ const canvasWidth = GameEnv.innerWidth;
+ const canvasHeight = canvasWidth / this.aspect_ratio;
+ GameEnv.backgroundHeight = canvasHeight;
+ const canvasLeft = 0;
+
+ this.canvas.width = this.width;
+ this.canvas.height = this.height;
+ this.canvas.style.width = `${canvasWidth}px`;
+ this.canvas.style.height = `${GameEnv.backgroundHeight}px`;
+ this.canvas.style.position = 'absolute';
+ this.canvas.style.left = `${canvasLeft}px`;
+ this.canvas.style.top = `${GameEnv.top}px`;
+
+ // set bottom of game to new background height
+ GameEnv.setBottom();
+ }
+}
+
+export default Background;
\ No newline at end of file
diff --git a/assets/js/platformer2/BackgroundHills.js b/assets/js/platformer2/BackgroundHills.js
new file mode 100644
index 0000000..54890a2
--- /dev/null
+++ b/assets/js/platformer2/BackgroundHills.js
@@ -0,0 +1,22 @@
+import GameEnv from './GameEnv.js';
+import Background from './Background.js';
+
+export class BackgroundHills extends Background {
+ constructor(canvas, image, data) {
+ super(canvas, image, data);
+ }
+
+ // speed is used to background parallax behavior
+ update() {
+ this.speed = GameEnv.backgroundHillsSpeed;
+ super.update();
+ }
+
+ draw() {
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ super.draw();
+ }
+
+}
+
+export default BackgroundHills;
\ No newline at end of file
diff --git a/assets/js/platformer2/BackgroundMountains.js b/assets/js/platformer2/BackgroundMountains.js
new file mode 100644
index 0000000..94b7885
--- /dev/null
+++ b/assets/js/platformer2/BackgroundMountains.js
@@ -0,0 +1,16 @@
+import GameEnv from './GameEnv.js';
+import Background from './Background.js';
+
+export class BackgroundMountains extends Background {
+ constructor(canvas, image, data) {
+ super(canvas, image, data);
+ }
+
+ // speed is used to background parallax behavior
+ update() {
+ this.speed = GameEnv.backgroundMountainsSpeed;
+ super.update();
+ }
+}
+
+export default BackgroundMountains;
\ No newline at end of file
diff --git a/assets/js/platformer2/Character.js b/assets/js/platformer2/Character.js
new file mode 100644
index 0000000..1d90b29
--- /dev/null
+++ b/assets/js/platformer2/Character.js
@@ -0,0 +1,132 @@
+import GameEnv from './GameEnv.js';
+import GameObject from './GameObject.js';
+
+class Character extends GameObject {
+ constructor(canvas, image, data) {
+ super(canvas, image, data);
+
+ // sprite sizes
+ this.spriteWidth = data.width;
+ this.spriteHeight = data.height;
+
+ // scale size
+ this.scaleSize = data?.scaleSize || 80;
+
+ // sprint frame management
+ this.minFrame = 0;
+ this.maxFrame = 0;
+ this.frameX = 0; // Default X frame of the animation
+ this.frameY = 0; // Default Y frame of the animation
+
+ // gravity for character enabled by default
+ this.gravityEnabled = true;
+ }
+
+ getMinFrame(){
+ return this.manFrame;
+ }
+
+ setMinFrame(minFrame){
+ this.minFrame = minFrame;
+ }
+
+ getMaxFrame(){
+ return this.maxFrame;
+ }
+
+ setMaxFrame(maxFrame){
+ this.maxFrame = maxFrame;
+ }
+
+ getFrameX() {
+ return this.frameX;
+ }
+
+ setFrameX(frameX){
+ this.frameX = frameX;
+ }
+
+ getFrameY() {
+ return this.frameY;
+ }
+
+ setFrameY(frameY){
+ this.frameY = frameY;
+ }
+
+ /* Draw character object
+ * Canvas and Context
+ */
+ draw() {
+ // Set fixed dimensions and position for the Character
+ this.canvas.width = this.canvasWidth;
+ this.canvas.height = this.canvasHeight;
+ this.canvas.style.width = `${this.canvas.width}px`;
+ this.canvas.style.height = `${this.canvas.height}px`;
+ this.canvas.style.position = 'absolute';
+ this.canvas.style.left = `${this.x}px`; // Set character horizontal position based on its x-coordinate
+ this.canvas.style.top = `${this.y}px`; // Set character up and down position based on its y-coordinate
+
+ this.ctx.drawImage(
+ this.image,
+ this.frameX * this.spriteWidth,
+ this.frameY * this.spriteHeight,
+ this.spriteWidth,
+ this.spriteHeight,
+ 0,
+ 0,
+ this.canvas.width,
+ this.canvas.height
+ );
+ }
+
+ /* Method should be called on initialization and resize events
+ * intent is to size character in proportion to the screen size
+ */
+ size() {
+ // set Canvas scale, 80 represents size of Character height when inner Height is 832px
+ var scaledCharacterHeight = GameEnv.innerHeight * (this.scaleSize / 832);
+ var canvasScale = scaledCharacterHeight/this.spriteHeight;
+ this.canvasHeight = this.spriteHeight * canvasScale;
+ this.canvasWidth = this.spriteWidth * canvasScale;
+
+ // set variables used in Display and Collision algorithms
+ this.bottom = GameEnv.bottom - this.canvasHeight;
+ this.collisionHeight = this.canvasHeight;
+ this.collisionWidth = this.canvasWidth;
+
+ // calculate Proportional x and y positions based on size of screen dimensions
+ if (GameEnv.prevInnerWidth) {
+ const proportionalX = (this.x / GameEnv.prevInnerWidth) * GameEnv.innerWidth;
+
+ // Update the x and y positions based on the proportions
+ this.setX(proportionalX);
+ this.setY(this.bottom);
+ } else {
+ // First Screen Position
+ this.setX(0);
+ this.setY(this.bottom);
+ }
+ }
+
+ /* Update cycle check collisions
+ * override draw for custom update
+ * be sure to have updated draw call super.update()
+ */
+ update() {
+
+ if (this.bottom > this.y && this.gravityEnabled)
+ this.y += GameEnv.gravity;
+
+ // Update animation frameX of the object
+ if (this.frameX < this.maxFrame) {
+ this.frameX++;
+ } else {
+ this.frameX = 0;
+ }
+
+ this.collisionChecks();
+ }
+}
+
+export default Character;
\ No newline at end of file
diff --git a/assets/js/platformer2/GameControl.js b/assets/js/platformer2/GameControl.js
new file mode 100644
index 0000000..348256e
--- /dev/null
+++ b/assets/js/platformer2/GameControl.js
@@ -0,0 +1,151 @@
+/**
+ * GameControl module.
+ * @module GameControl
+ * @description GameControl.js key objective is to control the game loop.
+ * Usage Notes:
+ * - call GameControl.gameLoop() to run the game levels.
+ * - call or add listener to GameControl.startTimer() to start the game timer.
+ */
+import GameEnv from './GameEnv.js';
+
+/**
+ * GameControl is a singleton object that controls the game loop.
+ * @namespace GameControl
+ *
+ * Coding Style Notes:
+ * - GameControl is defined as an object literal
+ * - GameControl is a singleton object, without a constructor.
+ * - This coding style ensures one instance, thus the term object literal.
+ * - Informerly, GameControl looks like defining a variable with methods contained inside.
+ * - The object literal style is a common pattern in JavaScript.
+ * - Observe, definition style of methods with GameControl.methodName = function() { ... }
+ * - Example: transitionToLevel(newLevel) { ... } versus transitionToLevel: function(newLevel) { ... }
+ * - Methods are defined as ES6 shorthand, versus the traditional function() style.
+ * - The shorthand style is a common pattern in JavaScript, more concise, and readable as it common to other coding languages.
+ * - But, it does not look like key-value pairs, which is the traditional object literal style.
+ * - This shorthand is part of ES6, and is supported by all modern browsers. references: https://caniuse.com/#feat=es6, https://www.w3schools.com/js/js_versions.asp
+ * - Observe, scoping/encapulation of this.inTransition and sharing data between methods.
+ * - this.inTransition is defined in the object literal scope.
+ * - this.inTransition is shared between methods.
+ * - this.inTransition is not accessible outside of the object literal scope.
+ * - this.inTransition is not a global or static variable.
+ *
+ */
+const GameControl = {
+ /**
+ * A reference to the interval used for the game timer.
+ * @type {number}
+ */
+ timerInterval: null, // Variable to hold the timer interval reference
+ /**
+ * The start time of the game timer.
+ * @type {number}
+ */
+ startTime: null, // Variable to hold the start time
+
+ /**
+ * Updates and displays the game timer.
+ * @function updateTimer
+ * @memberof GameControl
+ */
+ updateTimer() {
+ const id = document.getElementById("gameOver");
+ if (id.hidden == false) {
+ this.stopTimer()
+ }
+
+ // Calculate elapsed time in seconds
+ const elapsedTime = (Date.now() - this.startTime) / 1000;
+
+ // Display the updated time in the span element with id 'timeScore'
+ const timeScoreElement = document.getElementById('timeScore');
+ if (timeScoreElement) {
+ timeScoreElement.textContent = elapsedTime.toFixed(2); // Update the displayed time
+ }
+ },
+
+ /**
+ * Starts the game timer.
+ * @function startTimer
+ * @memberof GameControl
+ */
+ startTimer() {
+ // Get the current time
+ this.startTime = Date.now();
+
+ // Start the timer interval, updating the timer every 0.01 second (10 milliseconds)
+ this.timerInterval = setInterval(() => this.updateTimer(), 10);
+ },
+
+ /**
+ * Stops the game timer.
+ * @function stopTimer
+ * @memberof GameControl
+ */
+ stopTimer() {
+ clearInterval(this.timerInterval); // Clear the interval to stop the timer
+ },
+
+ /**
+ * Transitions to a new level. Destroys the current level and loads the new level.
+ * @param {Object} newLevel - The new level to transition to.
+ */
+ async transitionToLevel(newLevel) {
+ this.inTransition = true;
+
+ // Destroy existing game objects
+ GameEnv.destroy();
+
+ // Load GameLevel objects
+ await newLevel.load();
+ GameEnv.currentLevel = newLevel;
+
+ // Update invert property
+ GameEnv.setInvert();
+
+ // Trigger a resize to redraw canvas elements
+ window.dispatchEvent(new Event('resize'));
+
+ this.inTransition = false;
+ },
+
+ /**
+ * The main game control loop.
+ * Checks if the game is in transition. If it's not, it updates the game environment,
+ * checks if the current level is complete, and if it is, transitions to the next level.
+ * If the current level is null, it transitions to the beginning of the game.
+ * Finally, it calls itself again using requestAnimationFrame to create a loop.
+ */
+ gameLoop() {
+ // Turn game loop off during transitions
+ if (!this.inTransition) {
+
+ // Get current level
+ GameEnv.update();
+ const currentLevel = GameEnv.currentLevel;
+
+ // currentLevel is defined
+ if (currentLevel) {
+ // run the isComplete callback function
+ if (currentLevel.isComplete && currentLevel.isComplete()) {
+ const currentIndex = GameEnv.levels.indexOf(currentLevel);
+ // next index is in bounds
+ if (currentIndex !== -1 && currentIndex + 1 < GameEnv.levels.length) {
+ // transition to the next level
+ this.transitionToLevel(GameEnv.levels[currentIndex + 1]);
+ }
+ }
+ // currentLevel is null, (ie start or restart game)
+ } else {
+ // transition to beginning of game
+ this.transitionToLevel(GameEnv.levels[0]);
+ }
+ }
+
+ // recycle gameLoop, aka recursion
+ requestAnimationFrame(this.gameLoop.bind(this));
+ },
+
+};
+
+export default GameControl;
\ No newline at end of file
diff --git a/assets/js/platformer2/GameEnv.js b/assets/js/platformer2/GameEnv.js
new file mode 100644
index 0000000..689f69b
--- /dev/null
+++ b/assets/js/platformer2/GameEnv.js
@@ -0,0 +1,164 @@
+/**
+ * GameEnv.js key purpose is to manage shared game environment data and methods.
+ *
+ * @class
+ * @classdesc GameEnv is defined as a static class, this ensures that there is only one instance of the class.
+ * Static classes do not have a constructor, cannot be instantiated, do not have instance variables, only singleton/static variables,
+ * do not have instance methods, only singleton/static methods, is similar in namespace to an object literal, but is a class.
+ * The benefit is it is similar to other coding languages (e.g. Java, C#), thus is more readable to other developers.
+ *
+ * Purpose of GameEnv:
+ * - stores game objects (e.g. gameObjects, player, levels, etc.)
+ * - stores game attributes (e.g. gravity, speed, width, height, top, bottom, etc.)
+ * - defines methods to update, draw, and destroy game objects
+ * - defines methods to initialize and resize game objects
+ *
+ * Usage Notes:
+ * GameEnv is used by other classes to manage the game environment.
+ * It is dangerous to use GameEnv directly, it is not protected from misuse. Comments below show locations of usage.
+ * Here are some methods supported by GameEnv:
+ * - call GameEnv.initialize() to initialize window dimensions
+ * - call GameEnv.resize() to resize game objects
+ * - call GameEnv.update() to update, serialize, and draw game objects
+ * - call GameEnv.destroy() to destroy game objects
+ */
+export class GameEnv {
+
+ /**
+ * @static
+ * @property {string} userID - localstorage key, used by GameControl
+ * @property {Object} player - used by GameControl
+ * @property {Array} levels - used by GameControl
+ * @property {Object} currentLevel - used by GameControl
+ * @property {Array} gameObjects - used by GameControl
+ * @property {boolean} isInverted - localstorage key, canvas filter property, used by GameControl
+ * @property {number} gameSpeed - localstorage key, used by platformer objects
+ * @property {number} backgroundHillsSpeed - used by background objects
+ * @property {number} backgroundMountainsSpeed - used by background objects
+ * @property {number} gravity - localstorage key, used by platformer objects
+ * @property {number} innerWidth - used by platformer objects
+ * @property {number} prevInnerWidth - used by platformer objects
+ * @property {number} innerHeight - used by platformer objects
+ * @property {number} top - used by platformer objects
+ * @property {number} bottom - used by platformer objects
+ * @property {number} prevBottom - used by platformer objects
+ * @property {number} time - Initialize time variable, used by timer objects
+ * @property {number} timerInterval - Variable to hold the interval reference, used by timer objects
+ */
+ static userID = "Guest";
+ static player = null;
+ static levels = [];
+ static currentLevel = null;
+ static gameObjects = [];
+ static isInverted = false;
+ static gameSpeed = 2;
+ static backgroundHillsSpeed = 0;
+ static backgroundMountainsSpeed = 0;
+ static gravity = 3;
+ static innerWidth;
+ static prevInnerWidth;
+ static innerHeight;
+ static top;
+ static bottom;
+ static prevBottom;
+
+ // Make the constructor throws an error, or effectively make it a private constructor.
+ constructor() {
+ throw new Error('GameEnv is a static class and cannot be instantiated.');
+ }
+
+ /**
+ * Setter for Top position, called by initialize in GameEnv
+ * @static
+ */
+ static setTop() {
+ // set top of game as header height
+ const header = document.querySelector('header');
+ if (header) {
+ this.top = header.offsetHeight;
+ }
+ }
+
+ /**
+ * Setter for Bottom position, called by resize in GameEnv
+ * @static
+ */
+ static setBottom() {
+ // sets the bottom or gravity 0
+ this.bottom =
+ this.top + this.backgroundHeight;
+ }
+
+ /**
+ * Setup for Game Environment, called by transitionToLevel in GameControl
+ * @static
+ */
+ static initialize() {
+ // store previous for ratio calculations on resize
+ this.prevInnerWidth = this.innerWidth;
+ this.prevBottom = this.bottom;
+
+ // game uses available width and height
+ this.innerWidth = window.innerWidth;
+ this.innerHeight = window.innerHeight;
+ this.setTop();
+ //this.setBottom(); // must be called in platformer objects
+ }
+
+ /**
+ * Resize game objects, called by resize in GameControl
+ * @static
+ */
+ static resize() {
+ GameEnv.initialize(); // Update GameEnv dimensions
+
+ // Call the sizing method on all game objects
+ for (var gameObject of GameEnv.gameObjects){
+ gameObject.size();
+ }
+ }
+
+ /**
+ * Update, serialize, and draw game objects, called by update in GameControl
+ * @static
+ */
+ static update() {
+ // Update game state, including all game objects
+ for (const gameObject of GameEnv.gameObjects) {
+ gameObject.update();
+ gameObject.serialize();
+ gameObject.draw();
+ }
+ }
+
+ /**
+ * Destroy game objects, called by destroy in GameControl
+ * @static
+ */
+ static destroy() {
+ // Destroy objects in reverse order
+ for (var i = GameEnv.gameObjects.length - 1; i >= 0; i--) {
+ const gameObject = GameEnv.gameObjects[i];
+ gameObject.destroy();
+ }
+ GameEnv.gameObjects = [];
+ }
+
+ /**
+ * Set "canvas filter property" between inverted and normal, called by setInvert in GameControl
+ * @static
+ */
+ static setInvert() {
+ for (var gameObject of GameEnv.gameObjects){
+ if (gameObject.invert && !this.isInverted) { // toggle off
+ gameObject.canvas.style.filter = "none"; // remove filter
+ } else if (gameObject.invert && this.isInverted) { // toggle on
+ gameObject.canvas.style.filter = "invert(100%)"; // remove filter
+ } else {
+ gameObject.canvas.style.filter = "none"; // remove filter
+ }
+ }
+ }
+}
+
+export default GameEnv;
\ No newline at end of file
diff --git a/assets/js/platformer2/GameLevel.js b/assets/js/platformer2/GameLevel.js
new file mode 100644
index 0000000..69c2728
--- /dev/null
+++ b/assets/js/platformer2/GameLevel.js
@@ -0,0 +1,68 @@
+// GameLevel.js key objective is to load and intialize GameObject(s) for a level.
+import GameEnv from './GameEnv.js';
+
+/**
+ * The GameLevel class represents a level in the game.
+ * Each instance of GameLevel contains all the game objects that make up a level,
+ * and provides methods to load the images for these objects and create instances of them.
+ */
+class GameLevel {
+ /**
+ * Creates a new GameLevel.
+ * @param {Object} levelObject - An object containing the properties for the level.
+ */
+ constructor(levelObject) {
+ // The levelObjects property stores the levelObject parameter.
+ this.levelObjects = levelObject;
+ // The tag is a friendly name used to identify the level.
+ this.tag = levelObject?.tag;
+ // The passive property determines if the level is passive (i.e., not playable).
+ this.passive = levelObject?.passive;
+ // The isComplete property is a function that determines if the level is complete.
+ // build conditions to make determination of complete (e.g., all enemies defeated, player reached the end of the screen, etc.)
+ this.isComplete = levelObject?.callback;
+ // The gameObjects property is an array of the game objects for this level.
+ this.gameObjects = this.levelObjects?.objects || [];
+ // Each GameLevel instance is stored in the GameEnv.levels array.
+ GameEnv.levels.push(this);
+ }
+
+ /**
+ * Loads the images for the game objects and creates new instances of them.
+ * If any image fails to load, an error is logged and the game is halted.
+ */
+ async load() {
+ try {
+ for (const obj of this.gameObjects) {
+ if (obj.data.file) {
+ // Load the image for the game object.
+ obj.image = await this.loadImage(obj.data.file);
+ // Create a new canvas for the game object.
+ const canvas = document.createElement("canvas");
+ canvas.id = obj.id;
+ document.querySelector("#canvasContainer").appendChild(canvas);
+ // Create a new instance of the game object.
+ new obj.class(canvas, obj.image, obj.data);
+ }
+ }
+ } catch (error) {
+ console.error('Failed to load one or more GameLevel objects:', error);
+ }
+ }
+
+ /**
+ * Loads an image from a given source.
+ * @param {string} src - The source of the image.
+ * @returns {Promise} A promise that resolves with the loaded image.
+ */
+ async loadImage(src) {
+ return new Promise((resolve, reject) => {
+ const image = new Image();
+ image.src = src;
+ image.onload = () => resolve(image);
+ image.onerror = reject;
+ });
+ }
+}
+
+export default GameLevel;
\ No newline at end of file
diff --git a/assets/js/platformer2/GameObject.js b/assets/js/platformer2/GameObject.js
new file mode 100644
index 0000000..c12c532
--- /dev/null
+++ b/assets/js/platformer2/GameObject.js
@@ -0,0 +1,188 @@
+import GameEnv from './GameEnv.js';
+
+class GameObject {
+ // container for all game objects in game
+ constructor(canvas, image, data) {
+ this.x = 0;
+ this.y = 0;
+ this.frame = 0;
+ this.canvas = canvas;
+ this.ctx = canvas.getContext('2d');
+ this.image = image;
+ this.width = image.width; // from Image() width
+ this.height = image.height; // from Image() height
+ this.collisionWidth = 0;
+ this.collisionHeight = 0;
+ this.aspect_ratio = this.width / this.height;
+ this.speedRatio = data?.speedRatio || 0;
+ this.speed = GameEnv.gameSpeed * this.speedRatio;
+ this.invert = true;
+ this.collisionData = {};
+ this.jsonifiedElement = '';
+ // Add this object to the game object array so collision can be detected
+ // among other things
+ GameEnv.gameObjects.push(this);
+ }
+
+ // extract change from Game Objects into JSON
+ serialize() {
+ this.logElement();
+ }
+
+ // log Character element change
+ logElement() {
+ var jsonifiedElement = this.stringifyElement();
+ if (jsonifiedElement !== this.jsonifiedElement) {
+ //console.log(jsonifiedElement);
+ this.jsonifiedElement = jsonifiedElement;
+ }
+ }
+
+ // strigify Character key data
+ stringifyElement() {
+ var element = this.canvas;
+ if (element && element.id) {
+ // Convert the relevant properties of the element to a string for comparison
+ return JSON.stringify({
+ id: element.id,
+ width: element.width,
+ height: element.height,
+ style: element.style.cssText,
+ position: {
+ left: element.style.left,
+ top: element.style.top
+ },
+ filter: element.style.filter
+ });
+ }
+ }
+
+ // X position getter and setter
+ getX() {
+ return this.x;
+ }
+
+ setX(x) {
+ this.x = x;
+ }
+
+ // Y position getter and setter
+ getY() {
+ return this.y;
+ }
+
+ setY(y) {
+ this.y = y;
+ }
+
+ /* Destroy Game Object
+ * remove canvas element of object
+ * remove object from GameObject array
+ */
+ destroy() {
+ const index = GameEnv.gameObjects.indexOf(this);
+ if (index !== -1) {
+ // Remove the canvas from the DOM
+ this.canvas.parentNode.removeChild(this.canvas);
+ GameEnv.gameObjects.splice(index, 1);
+ }
+ }
+
+
+ /* Default collision action is no action
+ * override when you extend for custom action
+ */
+ collisionAction(){
+ // no action
+ }
+
+ /* Default floor action is no action
+ * override when you extend for custom action
+ */
+ floorAction(){
+ // no action
+ }
+
+ /* Collision checks
+ * uses GameObject isCollision to detect hit
+ * calls collisionAction on hit
+ */
+ collisionChecks() {
+ for (var gameObj of GameEnv.gameObjects){
+ if (this != gameObj ) {
+ this.isCollision(gameObj);
+ if (this.collisionData.hit){
+ this.collisionAction();
+ }
+ if (this.collisionData.atFloor) {
+ this.floorAction();
+ }
+ }
+ }
+ }
+
+ /* Collision detection method
+ * usage: if (player.isCollision(platform)) { // action }
+ */
+ isCollision(other) {
+ // Bounding rectangles from Canvas
+ const thisRect = this.canvas.getBoundingClientRect();
+ const otherRect = other.canvas.getBoundingClientRect();
+
+ // Calculate center points of rectangles
+ const thisCenterX = (thisRect.left + thisRect.right) / 2;
+ //const thisCenterY = (thisRect.top + thisRect.bottom) / 2;
+ const otherCenterX = (otherRect.left + otherRect.right) / 2;
+ //const otherCenterY = (otherRect.top + otherRect.bottom) / 2;
+
+ // Calculate hitbox constants
+ var widthPercentage = 0.5;
+ var heightPercentage = 0.5;
+ if (this.canvas.id === "player" && other.canvas.id === "jumpPlatform") {
+ heightPercentage = 0.0;
+ }
+ if (this.canvas.id === "goomba" && other.canvas.id === "player") {
+ heightPercentage = 0.2;
+ }
+ const widthReduction = thisRect.width * widthPercentage;
+ const heightReduction = thisRect.height * heightPercentage;
+
+ // Build hitbox by subtracting reductions from the left, right, top, and bottom
+ const thisLeft = thisRect.left + widthReduction;
+ const thisTop = thisRect.top + heightReduction;
+ const thisRight = thisRect.right - widthReduction;
+ const thisBottom = thisRect.bottom - heightReduction;
+
+ // Determine hit and touch points of hit
+ this.collisionData = {
+ hit: (
+ thisLeft < otherRect.right &&
+ thisRight > otherRect.left &&
+ thisTop < otherRect.bottom &&
+ thisBottom > otherRect.top
+ ),
+ atFloor: (GameEnv.bottom <= this.y),
+ touchPoints: {
+ this: {
+ id: this.canvas.id,
+ top: thisRect.bottom > otherRect.top,
+ bottom: (thisRect.bottom <= otherRect.top) && !(Math.abs(thisRect.bottom - otherRect.bottom) <= GameEnv.gravity),
+ left: thisCenterX > otherCenterX,
+ right: thisCenterX < otherCenterX,
+ },
+ other: {
+ id: other.canvas.id,
+ top: thisRect.bottom < otherRect.top,
+ bottom: (thisRect.bottom >= otherRect.top) && !(Math.abs(thisRect.bottom - otherRect.bottom) <= GameEnv.gravity),
+ left: thisCenterX < otherCenterX,
+ right: thisCenterX > otherCenterX,
+ x: otherRect.left,
+ },
+ },
+ };
+
+ }
+
+}
+
+export default GameObject;
\ No newline at end of file
diff --git a/assets/js/platformer2/GameSetup.js b/assets/js/platformer2/GameSetup.js
new file mode 100644
index 0000000..30d8193
--- /dev/null
+++ b/assets/js/platformer2/GameSetup.js
@@ -0,0 +1,313 @@
+// GameSehup.js Key objective is to define GameLevel objects and their assets.
+import GameEnv from './GameEnv.js';
+import GameLevel from './GameLevel.js';
+// To build GameLevels, each contains GameObjects from below imports
+import Background from './Background.js'
+import BackgroundHills from './BackgroundHills.js';
+import BackgroundMountains from './BackgroundMountains.js';
+import Platform from './Platform.js';
+import JumpPlatform from './JumpPlatform.js';
+import Player from './Player.js';
+import Tube from './Tube.js';
+import Goomba from './Goomba.js';
+
+/* Coding Style Notes
+ *
+ * GameSetup is defined as an object literal in in Name Function Expression (NFE) style
+ * * const GameSetup = function() { ... } is an NFE
+ * * NFEs are a common pattern in JavaScript, reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function
+ *
+ * * Informerly, inside of GameSetup it looks like defining keys and values that are functions.
+ * * * GameSetup is a singleton object, object literal, without a constructor.
+ * * * This coding style ensures one instance, thus the term object literal.
+ * * * Inside of GameSetup, the keys are functions, and the values are references to the functions.
+ * * * * The keys are the names of the functions.
+ * * * * The values are the functions themselves.
+ *
+ * * Observe, encapulation of this.assets and sharing data between methods.
+ * * * this.assets is defined in the object literal scope.
+ * * * this.assets is shared between methods.
+ * * * this.assets is not accessible outside of the object literal scope.
+ * * * this.assets is not a global variable.
+ *
+ * * Observe, the use of bind() to bind methods to the GameSetup object.
+ * * * * bind() ensures "this" inside of methods binds to "GameSetup"
+ * * * * this avoids "Temporal Dead Zone (TDZ)" error...
+ *
+ *
+ * Usage Notes
+ * * call GameSetup.initLevels() to setup the game levels and assets.
+ * * * the remainder of GameSetup supports initLevels()
+ *
+*/
+
+// Define the GameSetup object literal
+const GameSetup = {
+
+ /* ==========================================
+ * ===== Game Level Methods +++==============
+ * ==========================================
+ * Game Level methods support Game Play, and Game Over
+ * * Helper functions assist the Callback methods
+ * * Callback methods are called by the GameLevel objects
+ */
+
+ /**
+ * Helper function that waits for a button click event.
+ * @param {string} id - The HTML id or name of the button.
+ * @returns {Promise} - A promise that resolves when the button is clicked.
+ * References:
+ * * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
+ * * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve
+ */
+ waitForButton: function(id) {
+ // Returns a promise that resolves when the button is clicked
+ return new Promise((resolve) => {
+ const waitButton = document.getElementById(id);
+ // Listener function to resolve the promise when the button is clicked
+ const waitButtonListener = () => {
+ resolve(true);
+ };
+ // Add the listener to the button's click event
+ waitButton.addEventListener('click', waitButtonListener);
+ });
+ },
+
+ /* ==========================================
+ * ===== Game Level Call Backs ==============
+ * ==========================================
+ * Game Level callbacks are functions that return true or false
+ */
+
+ /**
+ * Start button callback.
+ * Unhides the gameBegin button, waits for it to be clicked, then hides it again.
+ * @async
+ * @returns {Promise} Always returns true.
+ */
+ startGameCallback: async function() {
+ const id = document.getElementById("gameBegin");
+ // Unhide the gameBegin button
+ id.hidden = false;
+
+ // Wait for the startGame button to be clicked
+ await this.waitForButton('startGame');
+ // Hide the gameBegin button after it is clicked
+ id.hidden = true;
+
+ return true;
+ },
+
+ /**
+ * Home screen exits on the Game Begin button.
+ * Checks if the gameBegin button is hidden, which means the game has started.
+ * @returns {boolean} Returns true if the gameBegin button is hidden, false otherwise.
+ */
+ homeScreenCallback: function() {
+ // gameBegin hidden means the game has started
+ const id = document.getElementById("gameBegin");
+ return id.hidden;
+ },
+
+ /**
+ * Level completion callback, based on Player off screen.
+ * Checks if the player's x position is greater than the innerWidth of the game environment.
+ * If it is, resets the player for the next level and returns true.
+ * If it's not, returns false.
+ * @returns {boolean} Returns true if the player's x position is greater than the innerWidth, false otherwise.
+ */
+ playerOffScreenCallBack: function() {
+ // console.log(GameEnv.player?.x)
+ if (GameEnv.player?.x > GameEnv.innerWidth) {
+ GameEnv.player = null; // reset for next level
+ return true;
+ } else {
+ return false;
+ }
+ },
+
+ /**
+ * Game Over callback.
+ * Unhides the gameOver button, waits for it to be clicked, then hides it again.
+ * Also sets the currentLevel of the game environment to null.
+ * @async
+ * @returns {Promise} Always returns true.
+ */
+ gameOverCallBack: async function() {
+ const id = document.getElementById("gameOver");
+ id.hidden = false;
+
+ // Wait for the restart button to be clicked
+ await this.waitForButton('restartGame');
+ id.hidden = true;
+
+ // Change currentLevel to start/restart value of null
+ GameEnv.currentLevel = null;
+
+ return true;
+ },
+
+ /* ==========================================
+ * ======= Data Definitions =================
+ * ==========================================
+ * Assets for the Game Objects defined in nested JSON key/value pairs
+ *
+ * * assets: contains definitions for all game objects, images, and properties
+ * * * 1st level: category (obstacles, platforms, backgrounds, players, enemies)
+ * * * 2nd level: item (tube, grass, mario, goomba)
+ * * * 3rd level: property (src, width, height, scaleSize, speedRatio, w, wa, wd, a, s, d)
+ */
+
+ assets: {
+ obstacles: {
+ tube: { src: "/images/platformer/obstacles/tube.png" },
+ },
+ platforms: {
+ grass: { src: "/images/platformer/platforms/grass.png" },
+ alien: { src: "/images/platformer/platforms/alien.png" },
+ bricks: { src: "/images/platformer/platforms/brick_wall.png" },
+ },
+ backgrounds: {
+ start: { src: "/images/platformer/backgrounds/home.png" },
+ hills: { src: "/images/platformer/backgrounds/hills.png" },
+ avenida: { src: "/images/platformer/backgrounds/night_clouds.jpg" }, //temporary change: just to get the game running
+ mountains: { src: "/images/platformer/backgrounds/mario_mountains.jpg" },
+ planet: { src: "/images/platformer/backgrounds/planet.jpg" },
+ castles: { src: "/images/platformer/backgrounds/castles.png" },
+ end: { src: "/images/platformer/backgrounds/game_over.png" }
+ },
+ players: {
+ mario: {
+ src: "/images/platformer/sprites/mario.png",
+ width: 256,
+ height: 256,
+ scaleSize: 80,
+ speedRatio: 0.7,
+ w: { row: 10, frames: 15 },
+ wa: { row: 11, frames: 15 },
+ wd: { row: 10, frames: 15 },
+ a: { row: 3, frames: 7, idleFrame: { column: 7, frames: 0 } },
+ s: { row: 12, frames: 15 },
+ d: { row: 2, frames: 7, idleFrame: { column: 7, frames: 0 } }
+ },
+ monkey: {
+ src: "/images/platformer/sprites/monkey.png",
+ width: 40,
+ height: 40,
+ scaleSize: 80,
+ speedRatio: 0.7,
+ w: { row: 9, frames: 15 },
+ wa: { row: 9, frames: 15 },
+ wd: { row: 9, frames: 15 },
+ a: { row: 1, frames: 15, idleFrame: { column: 7, frames: 0 } },
+ s: { row: 12, frames: 15 },
+ d: { row: 0, frames: 15, idleFrame: { column: 7, frames: 0 } }
+ },
+ lopez: {
+ src: "/images/platformer/sprites/lopezanimation.png",
+ width: 46,
+ height: 52.5,
+ scaleSize: 60,
+ speedRatio: 0.7,
+ w: {row: 1, frames: 3},
+ wa: {row: 1, frames: 3},
+ wd: {row: 2, frames: 3},
+ idle: { row: 6, frames: 1, idleFrame: {column: 1, frames: 0} },
+ a: { row: 1, frames: 3, idleFrame: { column: 1, frames: 0 } }, // Right Movement
+ s: {}, // Stop the movement
+ d: { row: 2, frames: 3, idleFrame: { column: 1, frames: 0 } }, // Left Movement
+ runningLeft: { row: 5, frames: 3, idleFrame: {column: 1, frames: 0} },
+ runningRight: { row: 4, frames: 3, idleFrame: {column: 1, frames: 0} },
+ }
+ },
+ enemies: {
+ goomba: {
+ src: "/images/platformer/sprites/goomba.png",
+ width: 448,
+ height: 452,
+ scaleSize: 60,
+ speedRatio: 0.7,
+ }
+ }
+ },
+
+ /* ==========================================
+ * ========== Game Level init ===============
+ * ==========================================
+ *
+ * Game Level sequence as defined in code below
+ * * a.) tag: "start" level defines button selection and cycles to the home screen
+ * * b.) tag: "home" defines background and awaits "start" button selection and cycles to 1st game level
+ * * c.) tag: "hills" and other levels before the tag: "end" define key gameplay levels
+ * * d.) tag: "end" concludes levels with game-over-screen background and replay selections
+ *
+ * Definitions of new Object creations and JSON text
+ * * 1.) "new GameLevel" adds game objects to the game environment.
+ * * * JSON key/value "tag" is for readability
+ * * * JSON "callback" contains function references defined above that terminate a GameLevel
+ * * * JSON "objects" contain zero to many "GameObject"(s)
+ * * 2.) "GameObject"(s) are defined using JSON text and include name, id, class, and data.
+ * * * JSON key/value "name" is for readability
+ * * * JSON "id" is a GameObject classification and may have program significance
+ * * * JSON "class" is the JavaScript class that defines the GameObject
+ * * J* SON "data" contains assets and properties for the GameObject
+ */
+
+ initLevels: function(path) { // ensure valid {{site.baseurl}} for path
+
+ // Add File location in assets relative to the root of the site
+ Object.keys(this.assets).forEach(category => {
+ Object.keys(this.assets[category]).forEach(item => {
+ this.assets[category][item]['file'] = path + this.assets[category][item].src;
+ });
+ });
+
+ // Home screen added to the GameEnv ...
+ new GameLevel( {tag: "start", callback: this.startGameCallback } );
+ const homeGameObjects = [
+ { name:'background', id: 'background', class: Background, data: this.assets.backgrounds.start }
+ ];
+ // Home Screen Background added to the GameEnv, "passive" means complementary, not an interactive level..
+ new GameLevel( {tag: "home", callback: this.homeScreenCallback, objects: homeGameObjects, passive: true } );
+
+ // Hills Game Level defintion...
+ const hillsGameObjects = [
+ // GameObject(s), the order is important to z-index...
+ { name: 'mountains', id: 'background', class: BackgroundMountains, data: this.assets.backgrounds.mountains },
+ { name: 'hills', id: 'background', class: BackgroundHills, data: this.assets.backgrounds.hills },
+ { name: 'grass', id: 'platform', class: Platform, data: this.assets.platforms.grass },
+ { name: 'bricks', id: 'jumpPlatform', class: JumpPlatform, data: this.assets.platforms.bricks },
+ { name: 'goomba', id: 'goomba', class: Goomba, data: this.assets.enemies.goomba },
+ { name: 'mario', id: 'player', class: Player, data: this.assets.players.mario },
+ { name: 'tube', id: 'tube', class: Tube, data: this.assets.obstacles.tube },
+ ];
+ // Hills Game Level added to the GameEnv ...
+ new GameLevel( {tag: "hills", callback: this.playerOffScreenCallBack, objects: hillsGameObjects } );
+
+ // Avenida Game Level definition...
+ const avenidaGameObjects = [
+ // GameObject(s), the order is important to z-index...
+ { name: 'avenida', id: 'background', class: Background, data: this.assets.backgrounds.avenida },
+ { name: 'grass', id: 'platform', class: Platform, data: this.assets.platforms.grass },
+ { name: 'goomba', id: 'goomba', class: Goomba, data: this.assets.enemies.goomba },
+ { name: 'lopez', id: 'player', class: Player, data: this.assets.players.lopez },
+ ];
+ // Avenida Game Level added to the GameEnv ...
+ new GameLevel( {tag: "avenida", callback: this.playerOffScreenCallBack, objects: avenidaGameObjects } );
+
+ // Game Over Level definition...
+ const endGameObjects = [
+ { name:'background', class: Background, id: 'background', data: this.assets.backgrounds.end}
+ ];
+ // Game Over screen added to the GameEnv ...
+ new GameLevel( {tag: "end", callback: this.gameOverCallBack, objects: endGameObjects } );
+ }
+}
+// Bind the methods to the GameSetup object, ensures "this" inside of methods binds to "GameSetup"
+// * * this avoids "Temporal Dead Zone (TDZ)" error...
+// * * * * "Cannot access 'GameSetup' before initialization", light reading TDZ (ha ha)...
+// * * * * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let#Temporal_Dead_Zone
+GameSetup.startGameCallback = GameSetup.startGameCallback.bind(GameSetup);
+GameSetup.gameOverCallBack = GameSetup.gameOverCallBack.bind(GameSetup);
+
+export default GameSetup;
\ No newline at end of file
diff --git a/assets/js/platformer2/Goomba.js b/assets/js/platformer2/Goomba.js
new file mode 100644
index 0000000..48f1d0c
--- /dev/null
+++ b/assets/js/platformer2/Goomba.js
@@ -0,0 +1,49 @@
+import Character from './Character.js';
+import GameEnv from './GameEnv.js';
+
+export class Goomba extends Character {
+ // constructors sets up Character object
+ constructor(canvas, image, data){
+ super(canvas, image, data );
+
+ //Initial Position of Goomba
+ this.x = .6 * GameEnv.innerWidth;
+ }
+
+ update() {
+ super.update();
+
+ // Check for boundaries
+ if (this.x <= 0 || (this.x + this.canvasWidth >= GameEnv.innerWidth) ) {
+ this.speed = -this.speed;
+ }
+
+ // Every so often change direction
+ if (Math.random() < 0.005) {
+ this.speed = Math.random() < 0.5 ? -this.speed : this.speed;
+ }
+
+ // Move the enemy
+ this.x += this.speed;
+ }
+
+ // Player action on collisions
+ collisionAction() {
+ if (this.collisionData.touchPoints.other.id === "tube") {
+ if (this.collisionData.touchPoints.other.left || this.collisionData.touchPoints.other.right) {
+ this.speed = -this.speed;
+ }
+ }
+ if (this.collisionData.touchPoints.other.id === "player") {
+ // Collision: Top of Goomba with Bottom of Player
+ if (this.collisionData.touchPoints.other.bottom) {
+ console.log("Bye Bye Goomba");
+ this.x = GameEnv.innerWidth + 1;
+ this.destroy();
+ }
+ }
+ }
+
+}
+
+export default Goomba;
\ No newline at end of file
diff --git a/assets/js/platformer2/JumpPlatform.js b/assets/js/platformer2/JumpPlatform.js
new file mode 100644
index 0000000..f26fca9
--- /dev/null
+++ b/assets/js/platformer2/JumpPlatform.js
@@ -0,0 +1,42 @@
+import GameEnv from './GameEnv.js';
+import GameObject from './GameObject.js';
+
+export class JumpPlatform extends GameObject {
+ constructor(canvas, image, data) {
+ super(canvas, image, data);
+ }
+
+ // Required, but no update action
+ update() {
+ }
+
+ // Draw position is always 0,0
+ draw() {
+ this.ctx.drawImage(this.image, 0, 0);
+ }
+
+ // Set platform position
+ size() {
+ // Formula for Height should be on constant ratio, using a proportion of 832
+ const scaledHeight = GameEnv.innerHeight * (30/832);
+ const scaledWidth = GameEnv.innerHeight * .1; // width of jump platform is 1/10 of height
+ const platformX = GameEnv.innerWidth * .2;
+ const platformY = (GameEnv.bottom - scaledHeight) * .8;
+
+ // set variables used in Display and Collision algorithms
+ this.bottom = platformY;
+ this.collisionHeight = scaledHeight;
+ this.collisionWidth = scaledWidth;
+
+ //this.canvas.width = this.width;
+ //this.canvas.height = this.height;
+ this.canvas.style.width = `${scaledWidth}px`;
+ this.canvas.style.height = `${scaledHeight}px`;
+ this.canvas.style.position = 'absolute';
+ this.canvas.style.left = `${platformX}px`;
+ this.canvas.style.top = `${platformY}px`;
+
+ }
+}
+
+export default JumpPlatform;
\ No newline at end of file
diff --git a/assets/js/platformer2/LocalStorage.js b/assets/js/platformer2/LocalStorage.js
new file mode 100644
index 0000000..8482023
--- /dev/null
+++ b/assets/js/platformer2/LocalStorage.js
@@ -0,0 +1,60 @@
+export class LocalStorage{
+ constructor(keys){
+ this.keys = keys;
+ console.log("browser local storage available: "+String(this.storageAvailable));
+ }
+
+ get storageAvailable(){ //checks if browser is able to use local storage
+ let type = "localStorage";
+ let storage;
+ try {
+ storage = window[type];
+ const x = "__storage_test__";
+ storage.setItem(x, x);
+ storage.removeItem(x);
+ return true;
+ } catch (e) {
+ return (
+ e instanceof DOMException &&
+ // everything except Firefox
+ (e.code === 22 ||
+ // Firefox
+ e.code === 1014 ||
+ // test name field too, because code might not be present
+ // everything except Firefox
+ e.name === "QuotaExceededError" ||
+ // Firefox
+ e.name === "NS_ERROR_DOM_QUOTA_REACHED") &&
+ // acknowledge QuotaExceededError only if there's something already stored
+ storage &&
+ storage.length !== 0
+ );
+ }
+ }
+
+ save(key){ //save a particular key
+ if(!this.storageAvailable){return}; //check if local storage is possible
+ window.localStorage.setItem(key,this[key]);
+ }
+
+ load(key){//load a particular key
+ if(!this.storageAvailable){return}; //check if local storage is possible
+ this[key] = window.localStorage.getItem(key);
+ }
+
+ saveAll(){ //saves data for all keys in this.keys
+ if(!this.storageAvailable){return}; //check if local storage is possible
+ Object.keys(this.keys).forEach(key => {
+ window.localStorage.setItem(key,this[key]);
+ });
+ }
+
+ loadAll(){//loads data from all keys in this.keys
+ if(!this.storageAvailable){return}; //check if local storage is possible
+ Object.keys(this.keys).forEach(key => {
+ this[key] = window.localStorage.getItem(key);
+ });
+ }
+}
+
+export default LocalStorage;
\ No newline at end of file
diff --git a/assets/js/platformer2/Platform.js b/assets/js/platformer2/Platform.js
new file mode 100644
index 0000000..57cc2af
--- /dev/null
+++ b/assets/js/platformer2/Platform.js
@@ -0,0 +1,50 @@
+import GameEnv from './GameEnv.js';
+import GameObject from './GameObject.js';
+
+export class Platform extends GameObject {
+ constructor(canvas, image, data) {
+ super(canvas, image, data);
+ }
+
+ /* Update uses modulo math to cycle to start at width extent
+ * x is position in cycle
+ * speed can be used to scroll faster
+ * width is extent of background image
+ */
+ update() {
+ this.x = (this.x - this.speed) % this.width;
+ }
+
+ /* To draws are used to capture primary frame and wrap around ot next frame
+ * x to y is primary draw
+ * x + width to y is wrap around draw
+ */
+ draw() {
+ this.ctx.drawImage(this.image, this.x, this.y);
+ this.ctx.drawImage(this.image, this.x + this.width, this.y);
+ }
+
+ /* Background camvas is set to screen
+ * the ADJUST contant elements portions of image that don't wrap well
+ * the GameEnv.top is a getter used to set canvas under Menu
+ * the GameEnv.bottom is setter used to establish game bottom at offsetHeight of canvas
+ */
+ size() {
+ // Update canvas size
+ const scaledHeight = GameEnv.backgroundHeight / 6;
+
+ const canvasWidth = GameEnv.innerWidth;
+ const canvasLeft = 0;
+ GameEnv.platformHeight = scaledHeight;
+
+ this.canvas.width = this.width;
+ this.canvas.height = this.height;
+ this.canvas.style.width = `${canvasWidth}px`;
+ this.canvas.style.height = `${GameEnv.platformHeight}px`;
+ this.canvas.style.position = 'absolute';
+ this.canvas.style.left = `${canvasLeft}px`;
+ this.canvas.style.top = `${GameEnv.bottom}px`;
+ }
+}
+
+export default Platform;
\ No newline at end of file
diff --git a/assets/js/platformer2/Player.js b/assets/js/platformer2/Player.js
new file mode 100644
index 0000000..3a5042c
--- /dev/null
+++ b/assets/js/platformer2/Player.js
@@ -0,0 +1,275 @@
+import GameEnv from './GameEnv.js';
+import Character from './Character.js';
+
+/**
+ * @class Player class
+ * @description Player.js key objective is to eent the user-controlled character in the game.
+ *
+ * The Player class extends the Character class, which in turn extends the GameObject class.
+ * Animations and events are activiated by key presses, collisions, and gravity.
+ * WASD keys are used by user to control The Player object.
+ *
+ * @extends Character
+ */
+export class Player extends Character{
+ // instantiation: constructor sets up player object
+ constructor(canvas, image, data){
+ super(canvas, image, data);
+ // Player Data is required for Animations
+ this.playerData = data;
+
+ // Player control data
+ this.pressedKeys = {};
+ this.movement = {up: true, down: true, left: true, right: true};
+ this.isIdle = true;
+ this.directionKey = "d"; // initially facing right
+
+ // Store a reference to the event listener function
+ this.keydownListener = this.handleKeyDown.bind(this);
+ this.keyupListener = this.handleKeyUp.bind(this);
+
+ // Add event listeners
+ document.addEventListener('keydown', this.keydownListener);
+ document.addEventListener('keyup', this.keyupListener);
+
+ GameEnv.player = this;
+ }
+
+ /**
+ * Helper methods for checking the state of the player.
+ * Each method checks a specific condition and returns a boolean indicating whether that condition is met.
+ */
+
+ // helper: player facing left
+ isFaceLeft() { return this.directionKey === "a"; }
+ // helper: left action key is pressed
+ isKeyActionLeft(key) { return key === "a"; }
+ // helper: player facing right
+ isFaceRight() { return this.directionKey === "d"; }
+ // helper: right action key is pressed
+ isKeyActionRight(key) { return key === "d"; }
+ // helper: dash key is pressed
+ isKeyActionDash(key) { return key === "s"; }
+
+ // helper: action key is in queue
+ isActiveAnimation(key) { return (key in this.pressedKeys) && !this.isIdle; }
+ // helper: gravity action key is in queue
+ isActiveGravityAnimation(key) {
+ var result = this.isActiveAnimation(key) && (this.bottom <= this.y || this.movement.down === false);
+
+ // return to directional animation (direction?)
+ if (this.bottom <= this.y || this.movement.down === false) {
+ this.setAnimation(this.directionKey);
+ }
+
+ return result;
+ }
+
+ /**
+ * This helper method that acts like an animation manager. Frames are set according to player events.
+ * - Sets the animation of the player based on the provided key.
+ * - The key is used to look up the animation frame and idle in the objects playerData.
+ * If the key corresponds to a left or right movement, the directionKey is updated.
+ *
+ * @param {string} key - The key representing the animation to set.
+ */
+ setAnimation(key) {
+ // animation comes from playerData
+ var animation = this.playerData[key]
+ // direction setup
+ if (this.isKeyActionLeft(key)) {
+ this.directionKey = key;
+ this.playerData.w = this.playerData.wa;
+ } else if (this.isKeyActionRight(key)) {
+ this.directionKey = key;
+ this.playerData.w = this.playerData.wd;
+ }
+ // set frame and idle frame
+ this.setFrameY(animation.row);
+ this.setMaxFrame(animation.frames);
+ if (this.isIdle && animation.idleFrame) {
+ this.setFrameX(animation.idleFrame.column)
+ this.setMinFrame(animation.idleFrame.frames);
+ }
+ }
+
+ /**
+ * gameloop: updates the player's state and position.
+ * In each refresh cycle of the game loop, the player-specific movement is updated.
+ * - If the player is moving left or right, the player's x position is updated.
+ * - If the player is dashing, the player's x position is updated at twice the speed.
+ * This method overrides Character.update, which overrides GameObject.update.
+ * @override
+ */
+ update() {
+ // Player moving right
+ if (this.isActiveAnimation("a")) {
+ if (this.movement.left) this.x -= this.speed; // Move to left
+ }
+ // Player moving left
+ if (this.isActiveAnimation("d")) {
+ if (this.movement.right) this.x += this.speed; // Move to right
+ }
+ // Player moving at dash speed left or right
+ if (this.isActiveAnimation("s")) {
+ const moveSpeed = this.speed * 2;
+ this.x += this.isFaceLeft() ? -moveSpeed : moveSpeed;
+ }
+ // Player jumping
+ if (this.isActiveGravityAnimation("w")) {
+ if (this.gravityEnabled) {
+ this.y -= (this.bottom * .50); // bottom jump height
+ } else if (this.movement.down===false) {
+ this.y -= (this.bottom * .30); // platform jump height
+ }
+ }
+
+ // Perform super update actions
+ super.update();
+ }
+
+ /**
+ * gameloop: respoonds to level change and game over destroy player object
+ * This method is used to remove the event listeners for keydown and keyup events.
+ * After removing the event listeners, it calls the parent class's destroy player object.
+ * This method overrides GameObject.destroy.
+ * @override
+ */
+ destroy() {
+ // Remove event listeners
+ document.removeEventListener('keydown', this.keydownListener);
+ document.removeEventListener('keyup', this.keyupListener);
+
+ // Call the parent class's destroy method
+ super.destroy();
+ }
+
+ /**
+ * gameloop: performs action on collisions
+ * Handles the player's actions when a collision occurs.
+ * This method checks the collision, type of game object, and then to determine action, e.g game over, animation, etc.
+ * Depending on the side of the collision, it performs player action, e.g. stops movement, etc.
+ * This method overrides GameObject.collisionAction.
+ * @override
+ */
+ collisionAction() {
+ if (this.collisionData.touchPoints.other.id === "tube") {
+ // Collision with the left side of the Tube
+ if (this.collisionData.touchPoints.other.left) {
+ this.movement.right = false;
+ }
+ // Collision with the right side of the Tube
+ if (this.collisionData.touchPoints.other.right) {
+ this.movement.left = false;
+ }
+ // Collision with the top of the player
+ if (this.collisionData.touchPoints.other.bottom) {
+ this.x = this.collisionData.touchPoints.other.x;
+ this.gravityEnabled = false; // stop gravity
+ // Pause for two seconds
+ setTimeout(() => { // animation in tube for 2 seconds
+ this.gravityEnabled = true;
+ setTimeout(() => { // move to end of screen for end of game detection
+ this.x = GameEnv.innerWidth + 1;
+ }, 1000);
+ }, 2000);
+ }
+ } else {
+ // Reset movement flags if not colliding with a tube
+ this.movement.left = true;
+ this.movement.right = true;
+ }
+ // Gomba left/right collision
+ if (this.collisionData.touchPoints.other.id === "goomba") {
+ // Collision with the left side of the Enemy
+ if (this.collisionData.touchPoints.other.left) {
+ // Game over
+ this.x = GameEnv.innerWidth + 1;
+ }
+ // Collision with the right side of the Enemy
+ if (this.collisionData.touchPoints.other.right) {
+ // Game over
+ this.x = GameEnv.innerWidth + 1;
+ }
+ }
+ // Jump platform collision
+ if (this.collisionData.touchPoints.other.id === "jumpPlatform") {
+ // Player is on top of the Jump platform
+ if (this.collisionData.touchPoints.this.top) {
+ this.movement.down = false; // enable movement down without gravity
+ this.gravityEnabled = false;
+ this.setAnimation(this.directionKey); // set animation to direction
+ }
+ }
+ // Fall Off edge of Jump platform
+ else if (this.movement.down === false) {
+ this.movement.down = true;
+ this.gravityEnabled = true;
+ }
+ }
+
+ /**
+ * Handles the keydown event.
+ * This method checks the pressed key, then conditionally:
+ * - adds the key to the pressedKeys object
+ * - sets the player's animation
+ * - adjusts the game environment
+ *
+ * @param {Event} event - The keydown event.
+ */
+ handleKeyDown(event) {
+ if (this.playerData.hasOwnProperty(event.key)) {
+ const key = event.key;
+ if (!(event.key in this.pressedKeys)) {
+ this.pressedKeys[event.key] = this.playerData[key];
+ this.setAnimation(key);
+ // player active
+ this.isIdle = false;
+ }
+ // dash action on
+ if (this.isKeyActionDash(key)) {
+ this.canvas.style.filter = 'invert(1)';
+ }
+ // parallax background speed starts on player movement
+ if (this.isKeyActionLeft(key)) {
+ GameEnv.backgroundHillsSpeed = -0.4;
+ GameEnv.backgroundMountainsSpeed = -0.1;
+ } else if (this.isKeyActionRight(key)) {
+ GameEnv.backgroundHillsSpeed = 0.4;
+ GameEnv.backgroundMountainsSpeed = 0.1;
+ }
+ }
+ }
+
+ /**
+ * Handles the keyup event.
+ * This method checks the released key, then conditionally stops actions from formerly pressed key
+ * *
+ * @param {Event} event - The keyup event.
+ */
+ handleKeyUp(event) {
+ if (this.playerData.hasOwnProperty(event.key)) {
+ const key = event.key;
+ if (event.key in this.pressedKeys) {
+ delete this.pressedKeys[event.key];
+ }
+ this.setAnimation(key);
+ // player idle
+ this.isIdle = true;
+ // dash action off
+ if (this.isKeyActionDash(key)) {
+ this.canvas.style.filter = 'invert(0)';
+ }
+ // parallax background speed halts on key up
+ if (this.isKeyActionLeft(key) || this.isKeyActionRight(key)) {
+ GameEnv.backgroundHillsSpeed = 0;
+ GameEnv.backgroundMountainsSpeed = 0;
+ }
+ }
+ }
+
+
+}
+
+
+export default Player;
\ No newline at end of file
diff --git a/assets/js/platformer2/SettingsControl.js b/assets/js/platformer2/SettingsControl.js
new file mode 100644
index 0000000..d4cb7b2
--- /dev/null
+++ b/assets/js/platformer2/SettingsControl.js
@@ -0,0 +1,366 @@
+// SettingsControl.js key purpose is key/value management for game settings.
+import LocalStorage from "./LocalStorage.js";
+import GameEnv from "./GameEnv.js";
+import GameControl from "./GameControl.js";
+
+/* Coding Style Notes
+ *
+ * SettingsControl is defined as a Class
+ * * SettingsControl contains a constructor.
+ * * SettingsControl.constructor() is called when SettingsControl is instantiated.
+ * * SettingsControl is instantiated in SettingsControl.sidebar().
+ * * This coding style allows multiple instances of SettingsControl.
+ * * This coding style is a common pattern in JavaScript and is very similar to Java.
+ * * Methods are defined as ES6 shorthand
+ *
+ *
+ * * Observe, instantiation/scoping/encapulation of this.keys
+ * * * The constructor makes an instance of this.keys by calling super(keys).
+ * * * * Observe the super(keys) call, this calls extended LocalStorage class constructor.
+ * * * * Review LocalStorage.js for more details.
+ *
+ * * SettingsControl manages keys following Model-View-Control (MVC) design pattern.
+ * * * Model is the LocalStorage class, which enables persistence of settings between sessions.
+ * * * View is the HTML/CSS sidebar, which displays and stores document elements in the DOM.
+ * * * Control is the SettingsControl class, which manages exchange of data between Model and View.
+ *
+ *
+ * Usage Notes
+ * * call SettingsControl.sidebar() to run the settings sidebar.
+ * * * the remainder of SettingsControl supports the sidebar and MVC design for settings keys/values.
+ *
+*/
+
+// define the SettingsControl class
+export class SettingsControl extends LocalStorage{
+ constructor(){ //default keys for localStorage
+ var keys = {
+ userID:"userID",
+ currentLevel:"currentLevel",
+ isInverted:"isInverted",
+ gameSpeed:"gameSpeed",
+ gravity:"gravity",
+ };
+ super(keys); //creates this.keys
+ }
+
+ /**
+ * Note. Separated from constructor so that class can be created before levels are addeda
+ *
+ * Initializes the SettingsControl instance.
+ * Loads all keys from local storage.
+ * For each key,
+ * * If it exists in local storage, loads and parses its value.
+ * * Else when the key does not exist in local storage, sets key to the corresponding GameEnv.js variable.
+ */
+ initialize(){
+ // Load all keys from local storage
+ this.loadAll();
+
+ /**
+ * Handles a key by checking if it exists in local storage and parsing its value.
+ * If the key does not exist in local storage, it sets the key to the current value of the game environment variable.
+ *
+ * @param {string} key - The localstorae key.
+ * @param {*} gameEnvVariable - The corresponding game environment variable.
+ * @param {function} [parser=(val) => val] - An optional function to parse the value from local storage.
+ * If no parser parameter/function is provided, (val) => val is unchanged.
+ * Else if parser is provided, the value is parsed ... e.g.:
+ * * (val) => vall === "true" parses the value as a boolean
+ * * (val) => parseFloat(val) parses the value as a floating point number
+ */
+ const handleKey = (key, gameEnvVariable, parser = (val) => val) => {
+ if (this[this.keys[key]]) {
+ return parser(this[this.keys[key]]);
+ } else {
+ this[this.keys[key]] = gameEnvVariable;
+ return gameEnvVariable;
+ }
+ };
+
+ /* Call the handleKey function to set up each game environment variable
+ * The handleKey function takes three parameters:
+ * * key - the local storage key
+ * * gameEnvVariable - the corresponding game environment variable
+ * * parser - an optional function to parse the value extracted from local storage
+ */
+ // 'userID', the value is parsed as a string
+ GameEnv.userID = handleKey('userID', GameEnv.userID);
+ // 'currentLevel', the value is parsed as a an index into the GameEnv.levels array
+ GameEnv.currentLevel = handleKey('currentLevel', GameEnv.levels[Number(this[this.keys.currentLevel])]);
+ // 'isInverted', the value is parsed to a boolean
+ GameEnv.isInverted = handleKey('isInverted', GameEnv.isInverted, (val) => val === "true");
+ // 'gameSpeed', the value is parsed to a floating point number
+ GameEnv.gameSpeed = handleKey('gameSpeed', GameEnv.gameSpeed, parseFloat);
+ // 'gravity', the value is parsed to a floating point number
+ GameEnv.gravity = handleKey('gravity', GameEnv.gravity, parseFloat);
+
+ // List for th 'userID' update event
+ window.addEventListener("userID", (e)=>{
+ // Update the userID value when a userID event is fired
+ this[this.keys.userID] = e.detail.userID();
+ // Update the userID value in the game environment
+ GameEnv.userID = this[this.keys.userID];
+ // Save the userID value to local storage
+ this.save(this.keys.userID);
+ });
+
+ // Listen for the 'resize' update event
+ window.addEventListener("resize",()=>{
+ // Update the current level index when the level changes
+ this[this.keys.currentLevel] = GameEnv.levels.indexOf(GameEnv.currentLevel);
+ // Save the current level index to local storage
+ this.save(this.keys.currentLevel);
+ });
+
+ // Listen for the 'isInverted' update event
+ window.addEventListener("isInverted", (e)=>{
+ // Update the isInverted value when an invert event is fired
+ this[this.keys.isInverted] = e.detail.isInverted();
+ // Update the isInverted value in the game environment
+ GameEnv.isInverted = this[this.keys.isInverted];
+ // Save the isInverted value to local storage
+ this.save(this.keys.isInverted);
+ });
+
+ // Listen for the 'gameSpeed' update event
+ window.addEventListener("gameSpeed",(e)=>{
+ // Update the gameSpeed value when a speed event is fired
+ this[this.keys.gameSpeed] = e.detail.gameSpeed();
+ // Update the gameSpeed value in the game environment
+ GameEnv.gameSpeed = parseFloat(this[this.keys.gameSpeed]);
+ // Save the gameSpeed value to local storage
+ this.save(this.keys.gameSpeed);
+ });
+
+ // Listen for the 'gravity' update event
+ window.addEventListener("gravity",(e)=>{
+ // Update the gravity value when a gravity event is fired
+ this[this.keys.gravity] = e.detail.gravity();
+ // Update the gravity value in the game environment
+ GameEnv.gravity = parseFloat(this[this.keys.gravity]);
+ // Save the gravity value to local storage
+ this.save(this.keys.gravity);
+ });
+
+ }
+
+ /**
+ * Getter for the userID property.
+ * Creates a div with a text input for the user to enter a userID.
+ * The input's value is bound to the GameEnv's userID string.
+ * @returns {HTMLDivElement} The div containing the userID input.
+ */
+ get userIDInput() {
+ const div = document.createElement("div");
+ div.innerHTML = "User ID: "; // label
+
+ const userID = document.createElement("input"); // get user defined userID
+ userID.type = "text";
+ userID.value = GameEnv.userID; // GameEnv contains latest userID
+ userID.maxLength = 10; // set maximum length to 10 characters
+ userID.className = "input userID"; // custom style in platformer-styles.scss
+
+ userID.addEventListener("change", () => {
+ // dispatch event to update userID
+ window.dispatchEvent(new CustomEvent("userID", { detail: {userID:()=>userID.value} }));
+ });
+
+ div.append(userID); // wrap input element in div
+ return div;
+ }
+
+ /**
+ * Getter for the levelTable property.
+ * Creates a table with a row for each game level.
+ * Each row contains the level number and the level tag.
+ * Passive levels are skipped and not added to the table.
+ * @returns {HTMLTableElement} The table containing the game levels.
+ */
+ get levelTable(){
+ // create table element
+ var t = document.createElement("table");
+ t.className = "table levels";
+ //create table header
+ var header = document.createElement("tr");
+ var th1 = document.createElement("th");
+ th1.innerText = "#";
+ header.append(th1);
+ var th2 = document.createElement("th");
+ th2.innerText = "Level Tag";
+ header.append(th2);
+ t.append(header);
+
+ // Create table rows/data
+ for(let i = 0, count = 1; i < GameEnv.levels.length; i++){
+ if (GameEnv.levels[i].passive) //skip passive levels
+ continue;
+ // add level to table
+ var row = document.createElement("tr");
+ var td1 = document.createElement("td");
+ td1.innerText = String(count++); //human counter
+ row.append(td1);
+ // place level name in button
+ var td2 = document.createElement("td");
+ td2.innerText = GameEnv.levels[i].tag;
+ row.append(td2);
+ // listen for row click
+ row.addEventListener("click",()=>{ // when player clicks on the row
+ //transition to selected level
+ GameControl.transitionToLevel(GameEnv.levels[i]); // resize event is triggered in transitionToLevel
+ })
+ // add level row to table
+ t.append(row);
+ }
+
+ return t; //returns element
+ }
+
+ /**
+ * Getter for the isInvertedInput property.
+ * Creates a div with a checkbox input for the user to invert the game controls.
+ * The checkbox's checked state is bound to the GameEnv's isInverted state.
+ * @returns {HTMLDivElement} The div containing the isInverted checkbox.
+ */
+ get isInvertedInput() {
+ const div = document.createElement("div");
+ div.innerHTML = "Invert: "; // label
+
+ const isInverted = document.createElement("input"); // get user defined invert boolean
+ isInverted.type = "checkbox";
+ isInverted.checked = GameEnv.isInverted; // GameEnv contains latest isInverted state
+
+ isInverted.addEventListener("change", () => {
+ //`dispatch event to update isInverted
+ window.dispatchEvent(new CustomEvent("isInverted", { detail: {isInverted:()=>isInverted.checked} }));
+ });
+
+ div.append(isInverted); // wrap input element in div
+ return div;
+ }
+
+ /**
+ * Getter for the gameSpeedInput property.
+ * Creates a div with a number input for the user to adjust the game speed.
+ * The input's value is bound to the GameEnv's gameSpeed state.
+ * @returns {HTMLDivElement} The div containing the gameSpeed input.
+ */
+ get gameSpeedInput() {
+ const div = document.createElement("div");
+ div.innerHTML = "Game Speed: "; // label
+
+ const gameSpeed = document.createElement("input"); // get user defined game speed
+ gameSpeed.type = "number";
+ gameSpeed.min = 1.0;
+ gameSpeed.max = 8.0;
+ gameSpeed.step = 0.1;
+ gameSpeed.default = 2.0; // customed property for default value
+ gameSpeed.value = GameEnv.gameSpeed; // GameEnv contains latest game speed
+ gameSpeed.className = "input gameSpeed"; // custom style in platformer-styles.scss
+
+ gameSpeed.addEventListener("change", () => {
+ // check values are within range
+ const value = parseFloat(gameSpeed.value).toFixed(1);
+ gameSpeed.value = (value < gameSpeed.min || value > gameSpeed.max || isNaN(value)) ? gameSpeed.default : value;
+ // dispatch event to update game speed
+ window.dispatchEvent(new CustomEvent("gameSpeed", { detail: {gameSpeed:()=>gameSpeed.value} }));
+ });
+
+ div.append(gameSpeed); // wrap input element in div
+ return div;
+ }
+
+ /**
+ * Getter for the gravityInput property.
+ * Creates a div with a number input for the user to adjust the game gravity.
+ * The input's value is bound to the GameEnv's gravity state.
+ * @returns {HTMLDivElement} The div containing the gravity input.
+ */
+ get gravityInput() {
+ const div = document.createElement("div");
+ div.innerHTML = "Gravity: "; // label
+
+ const gravity = document.createElement("input"); // get user defined gravity
+ gravity.type = "number";
+ gravity.min = 1.0;
+ gravity.max = 8.0;
+ gravity.step = 0.1;
+ gravity.default = 3.0; // customed property for default value
+ gravity.value = GameEnv.gravity; // GameEnv contains latest gravity
+ gravity.className = "input gravity"; // custom style in platformer-styles.scss
+
+ gravity.addEventListener("change", () => {
+ // check values are within range
+ const value = parseFloat(gravity.value).toFixed(1);
+ gravity.value = (value < gravity.min || value > gravity.max || isNaN(value)) ? gravity.default : value;
+ // dispatch event to update gravity
+ window.dispatchEvent(new CustomEvent("gravity", { detail: {gravity:()=>gravity.value} }));
+ });
+
+ div.append(gravity); // wrap input element in div
+ return div;
+ }
+
+ /**
+ * Static method to initialize the game settings controller and add the settings controls to the sidebar.
+ * Constructs an HTML table/menu from GameEnv.levels[] and HTML inputs for invert, game speed, and gravity.
+ * Each input has an event update associated with it.
+ * All elements are appended to the sidebar.
+ */
+ static sidebar(){
+ // Initiliaze Game settings controller
+ var settingsControl = new SettingsControl();
+ settingsControl.initialize();
+
+ // Get/Construct an HTML input for userID
+ var userID = settingsControl.userIDInput;
+ document.getElementById("sidebar").append(userID);
+
+ // Create a new div element to act as a spacer
+ var spacer = document.createElement("div");
+ spacer.style.height = "20px"; // Set the height of the spacer
+ document.getElementById("sidebar").append(spacer); // Add the spacer to the sidebar
+
+ // Get/Construct an HTML table/menu from GameEnv.levels[]
+ var levels = settingsControl.levelTable;
+ document.getElementById("sidebar").append(levels);
+
+ // Get/Construct HTML input and event update for invert
+ var invertControl = settingsControl.isInvertedInput;
+ document.getElementById("sidebar").append(invertControl);
+
+ // Get/Construct HTML input and event update for game speed
+ var gameSpeed = settingsControl.gameSpeedInput;
+ document.getElementById("sidebar").append(gameSpeed);
+
+ // Get/Construct HTML input and event update for gravity
+ var gravityInput = settingsControl.gravityInput;
+ document.getElementById("sidebar").append(gravityInput);
+
+ // Listener, isOpen, and function for sidebar open and close
+ var isOpen = false; // default sidebar is closed
+ var submenuHeight = 0; // calculated height of submenu
+ function sidebarPanel(){
+ // toggle isOpen
+ isOpen = !isOpen;
+ // open and close properties for sidebar based on isOpen
+ var sidebar = document.querySelector('.sidebar');
+ sidebar.style.width = isOpen?"200px":"0px";
+ sidebar.style.paddingLeft = isOpen?"10px":"0px";
+ sidebar.style.paddingRight = isOpen?"10px":"0px";
+ sidebar.style.top = `calc(${submenuHeight}px + ${GameEnv.top}px)`;
+ }
+ // settings-button and event listener opens sidebar
+ document.getElementById("settings-button").addEventListener("click",sidebarPanel);
+ // sidebar-header and event listener closes sidebar
+ document.getElementById("sidebar-header").addEventListener("click",sidebarPanel);
+
+ window.addEventListener('load', function() {
+ var submenu = document.querySelector('.submenu');
+ submenuHeight = submenu.offsetHeight;
+ });
+ }
+
+}
+
+export default SettingsControl;
\ No newline at end of file
diff --git a/assets/js/platformer2/Tube.js b/assets/js/platformer2/Tube.js
new file mode 100644
index 0000000..12174ff
--- /dev/null
+++ b/assets/js/platformer2/Tube.js
@@ -0,0 +1,43 @@
+import GameEnv from './GameEnv.js';
+import GameObject from './GameObject.js';
+
+export class Tube extends GameObject {
+ constructor(canvas, image) {
+ super(canvas, image, 0);
+ }
+
+ // Required, but no update action
+ update() {
+ }
+
+ // Draw position is always 0,0
+ draw() {
+ this.ctx.drawImage(this.image, 0, 0);
+ }
+
+ // Set Tube position
+ size() {
+ // Formula for Height should be on constant ratio, using a proportion of 832
+ const scaledHeight = GameEnv.innerHeight * (100 / 832);
+ // Formula for Width is scaled: scaledWidth/scaledHeight == this.width/this.height
+ const scaledWidth = scaledHeight * this.aspect_ratio;
+ const tubeX = .80 * GameEnv.innerWidth;
+ const tubeY = (GameEnv.bottom - scaledHeight);
+
+ // set variables used in Display and Collision algorithms
+ this.bottom = tubeY;
+ this.collisionHeight = scaledHeight;
+ this.collisionWidth = scaledWidth;
+
+ //this.canvas.width = this.width;
+ //this.canvas.height = this.height;
+ this.canvas.style.width = `${scaledWidth}px`;
+ this.canvas.style.height = `${scaledHeight}px`;
+ this.canvas.style.position = 'absolute';
+ this.canvas.style.left = `${tubeX}px`;
+ this.canvas.style.top = `${tubeY}px`;
+
+ }
+}
+
+export default Tube;
\ No newline at end of file
diff --git a/images/platformer/backgrounds/mario_mountains.jpg b/images/platformer/backgrounds/mario_mountains.jpg
new file mode 100644
index 0000000..8fc8888
Binary files /dev/null and b/images/platformer/backgrounds/mario_mountains.jpg differ
diff --git a/images/platformer/backgrounds/mountains.jpg b/images/platformer/backgrounds/mountains.jpg
new file mode 100644
index 0000000..c32e6f7
Binary files /dev/null and b/images/platformer/backgrounds/mountains.jpg differ
diff --git a/images/platformer/obstacles/brick_wall.png b/images/platformer/platforms/brick_wall.png
similarity index 100%
rename from images/platformer/obstacles/brick_wall.png
rename to images/platformer/platforms/brick_wall.png