diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5a0c7f1e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.DS_Store +node_modules/ +package-lock.json +private/ \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..28a804d8 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 717af5a6..6bd4aab6 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,45 @@ -# cs4241-FinalProject - -For your final project, you'll implement a course project that exhibits your mastery of the course materials. -Similar to A4, this project gives you an opportunity to be creative and to pursue individual research and learning. - -## General description - -Your project should consist of a complete Web application, exhibiting facets of the three main sections of the course material: - -- Static Web page content and design. You should have a project that is accessible, easily navigable, and features significant content. -- Dynamic behavior implemented with JavaScript. -- Server-side programming. Typically this will take the form of some sort of persistent data, authentication, and possibly server-side computation. - -Additionally, you should incorporate features that you independently research, design, and implement for your project. - -## Project ideation - -Excellent projects serve someone/some group; for this assignment you need to define your users and stakeholders. I encourage you to identify projects that will have impact, either artistically, politically, or in terms of productivity. Consider creating something useful for a cause or hobby you care about. - -## Logistics - -### Team size -Students are encouraged to work in teams of 2-5 students for the project. This will allow you to build a good project without expending an excessive amount of effort. While I would expect a team of four or five students to produce a project with more features, I expect a every team's work to exhibit all of the required facets described above. - -### Deliverables - -__Proposal:__ -Provide an outline of your project direction and the names of the team members. -The outline should have enough detail so that staff can determine if it meets the minimum expectations, or if it goes too far to be reasonable by the deadline. -This file must be named proposal.md so we can find it. -Submit a PR to turn it in by Monday, September 30th, before class - -There are no other scheduled checkpoints for your project. -You must be done in time to present before the final project demo day (October 10th). - -#### Turning in Your Outline / Project - -**NOTE: code is due before the project presentation day due to the end of term / grading schedule constraints** -Submit a second PR on the final project repo to turn in your app and code. - -Deploy your app, in the form of a webpage, to Glitch/Heroku/Digital Ocean or some other service. -Folks on the same team do not need to post the same webpage, but must instead clearly state who is on the team in their proposal. -(Staff will use the proposal to build the grading sheet.) - -## Final Presentation - -Presentations will occur during the final day of class. - -## FAQs - -- **Can I use XYZ framework?** You can use any web-based frameworks or tools available. +# AJAJA Games + +Link: https://ajaja-games.glitch.me/ + +A game website which hosts simple arcade and puzzle games that can be played in most web browsers. The website's main interface will feature the games created over the course of the project as well as a way to login to save scores. + +## Team +- Andrew Bonaventura +- Andrew Markoski +- Andrew Nolan +- James Kajon +- James Plante + +## Games +- Luck - Click the pot of gold to get your luck percentage. +- Brick Destroyer - a game similar to Breakout where you hit bricks with a ball to make them disappear. +- Flappy Goat - a game similar to Flappy Bird, except with Gompei as the mascot +- Gomepi Run - a game similar to endless running games such as the dinosaur easter egg in Google Chrome or Canabalt, where Gompei is running at a set speed while avoiding incoming boulders. +- Open Tower Defense - a basic tower defense game where you have to defend you base from oncoming waves of tanks. +- Sudoku - an implementation of the classic Soduku puzzle game. Be sure to get the fastest time! +- Kenken - a simular game to Soduku, with additional stipulations +- Rock, Paper, Scissors - Play ten rounds of rock, paper, scissors. +- Epic Game - Test game to test the high score system. + +## Instructions +- In order to use the website to save high scores, you can either log in with your GitHub account, Google account, or create a new account. + +## Technologies Used +- Handlebars - Builds templates for each of our pages +- Bootstrap - CSS framework that was used to style each of the pages +- Phaser - JS framework used to create some of the games with active physics such as Gompei Run, Open Tower Defense, Luck, and Brick Destroyer +- Express - Used for the server +- Firebase - Used for database storage and authentication for users. +- Canvas - Used for some games such as Flappy Goat. + +## Main Challenges +- Learning new technologies to acoomplish game specific features such as Phaser to make physics easier. +- Incoroporating Firebase into the project for storing scores and incorporating this functionality into each game. + +## Group Member Responsibilities +- Andrew Bonaventura - Creation of Flappy Goat +- Andrew Markoski - Cration of Gompei Run +- Andrew Nolan - Creation of Sudoku, Rock Paper Scissors, and Kenken +- James Kajon - Creation of main site, setting up and maintaining database, creation of Open Tower Defense, creation of leaderboard system and authentication +- James Plante - Creation of Brick Destroyer, deployed site on Glitch. diff --git a/app.js b/app.js new file mode 100644 index 00000000..4967b932 --- /dev/null +++ b/app.js @@ -0,0 +1,51 @@ +const express = require("express"), + hbs = require("express-handlebars"), + path = require("path"), + morgan = require("morgan"), + compression = require("compression"), + helmet = require("helmet"), + bodyParser = require("body-parser"), + firebaseAdmin = require("firebase-admin"); + +const app = express(); +const port = process.env.PORT | 3000; + +const serviceAccount = require("./private/serviceAccountCredentials.json"); + +firebaseAdmin.initializeApp({ + credential: firebaseAdmin.credential.cert(serviceAccount), + databaseURL: "https://cs4241-fp-26fee.firebaseio.com" +}); + +const authRouter = require("./routes/auth-routes"), + gameRouter = require("./routes/game-routes"), + gameDataRouter = require("./routes/game-data-routes"), + indexRouter = require("./routes/index-routes"); + + +// morgan logger +app.use(morgan("dev")); +app.use(compression()); +app.use(helmet()); + +// template engine setup (handlebars) +app.engine("hbs", hbs({helpers: require("./views/helpers.js").helpers, extname: "hbs", defaultLayout: "layout", layoutsDir: __dirname + "/views/layouts/"})); +// app.engine("hbs", hbs({extname: "hbs", defaultLayout: "layout", layoutsDir: __dirname + "/views/layouts/"})); +app.set("views", path.join(__dirname, "views")); +app.set("view engine", "hbs"); + +app.use(express.static("public")); +app.use("/scripts", express.static(__dirname + "/node_modules/")); + +app.use(bodyParser.json()); // to support JSON-encoded bodies +app.use(bodyParser.urlencoded({ // to support URL-encoded bodies + extended: false +})); + +app.use("/data/games", gameDataRouter); // store game data +app.use("/games", gameRouter); // load game pages +app.use("/", indexRouter); // load game pages +app.use("/auth", authRouter); // handel sign ups + + +app.listen(port, () => console.log(`Listening on port ${port}`)); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..a41c4384 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "fp", + "version": "1.0.0", + "scripts": { + "start": "node app.js" + }, + "dependencies": { + "body-parser": "latest", + "compression": "^1.7.4", + "express": "^4.17.1", + "express-handlebars": "^3.1.0", + "firebase-admin": "^8.6.0", + "helmet": "^3.21.1", + "morgan": "^1.9.1", + "phaser": "^3.19.0", + "webpack": "^4.41.0" + } +} diff --git a/proposal.md b/proposal.md new file mode 100644 index 00000000..bece76ef --- /dev/null +++ b/proposal.md @@ -0,0 +1,50 @@ +# AJAJA Games + +A game website which hosts simple arcade and puzzle games that can be played in most web browsers. The website's main interface will feature the games created over the course of the project as well as a way to login to save scores. + +## Team +- Andrew Bonaventura +- Andrew Markoski +- Andrew Nolan +- James Kajon +- James Plante + + +## Requirements + +### Static + +Our website will have one main webpage where users can choose a game to play as well as a page for each game. + +### Dynamic + +All of our games will be created using JavaScript. + +### Server-side + +Our website will have game scores stored in a database using Firebase. Users will need to sign in to save their score. + + +## Games + +### Currently Developing + +- Breakout +- Rocket Lander +- Running game (like Chrome dino run) +- Asteroids +- Sudoku + +### Possible Future + +- 3D running game +- Pinball +- Air Hockey (against simple AI) +- Billiards +- Tetris +- Tic-tac-toe +- Hangman +- Puzzle games +- Memory/match game +- Typing test + diff --git a/public/assets/bdest/ball.png b/public/assets/bdest/ball.png new file mode 100644 index 00000000..55a8c944 Binary files /dev/null and b/public/assets/bdest/ball.png differ diff --git a/public/assets/bdest/bar_orange.png b/public/assets/bdest/bar_orange.png new file mode 100644 index 00000000..31255ef5 Binary files /dev/null and b/public/assets/bdest/bar_orange.png differ diff --git a/public/assets/bdest/bar_red.png b/public/assets/bdest/bar_red.png new file mode 100644 index 00000000..279d2498 Binary files /dev/null and b/public/assets/bdest/bar_red.png differ diff --git a/public/assets/bdest/bar_yellow.png b/public/assets/bdest/bar_yellow.png new file mode 100644 index 00000000..41f7aad4 Binary files /dev/null and b/public/assets/bdest/bar_yellow.png differ diff --git a/public/assets/bdest/paddle.png b/public/assets/bdest/paddle.png new file mode 100644 index 00000000..926b9f19 Binary files /dev/null and b/public/assets/bdest/paddle.png differ diff --git a/public/assets/flappyGoat/bluesky.jpg b/public/assets/flappyGoat/bluesky.jpg new file mode 100644 index 00000000..bd861106 Binary files /dev/null and b/public/assets/flappyGoat/bluesky.jpg differ diff --git a/public/assets/flappyGoat/flappyGoat.png b/public/assets/flappyGoat/flappyGoat.png new file mode 100644 index 00000000..5858bee5 Binary files /dev/null and b/public/assets/flappyGoat/flappyGoat.png differ diff --git a/public/assets/gompei/gompei.jpeg b/public/assets/gompei/gompei.jpeg new file mode 100644 index 00000000..dc3489d9 Binary files /dev/null and b/public/assets/gompei/gompei.jpeg differ diff --git a/public/assets/gompei/gompei.png b/public/assets/gompei/gompei.png new file mode 100644 index 00000000..6848c8f1 Binary files /dev/null and b/public/assets/gompei/gompei.png differ diff --git a/public/assets/gompei/grassLong.png b/public/assets/gompei/grassLong.png new file mode 100644 index 00000000..8e7ca259 Binary files /dev/null and b/public/assets/gompei/grassLong.png differ diff --git a/public/assets/gompei/ground.jpg b/public/assets/gompei/ground.jpg new file mode 100644 index 00000000..75a41f96 Binary files /dev/null and b/public/assets/gompei/ground.jpg differ diff --git a/public/assets/gompei/groundLong.png b/public/assets/gompei/groundLong.png new file mode 100644 index 00000000..e48b6c0c Binary files /dev/null and b/public/assets/gompei/groundLong.png differ diff --git a/public/assets/gompei/rock.png b/public/assets/gompei/rock.png new file mode 100644 index 00000000..06524316 Binary files /dev/null and b/public/assets/gompei/rock.png differ diff --git a/public/assets/images/bdest.png b/public/assets/images/bdest.png new file mode 100644 index 00000000..ee465e06 Binary files /dev/null and b/public/assets/images/bdest.png differ diff --git a/public/assets/images/ex-game.png b/public/assets/images/ex-game.png new file mode 100644 index 00000000..b682816d Binary files /dev/null and b/public/assets/images/ex-game.png differ diff --git a/public/assets/images/favicon-16x16.png b/public/assets/images/favicon-16x16.png new file mode 100644 index 00000000..0794bd18 Binary files /dev/null and b/public/assets/images/favicon-16x16.png differ diff --git a/public/assets/images/favicon-32x32.png b/public/assets/images/favicon-32x32.png new file mode 100644 index 00000000..d7f09de3 Binary files /dev/null and b/public/assets/images/favicon-32x32.png differ diff --git a/public/assets/images/flappygoat.jpg b/public/assets/images/flappygoat.jpg new file mode 100644 index 00000000..469df610 Binary files /dev/null and b/public/assets/images/flappygoat.jpg differ diff --git a/public/assets/images/gompei.png b/public/assets/images/gompei.png new file mode 100644 index 00000000..75c941cc Binary files /dev/null and b/public/assets/images/gompei.png differ diff --git a/public/assets/images/kenken.png b/public/assets/images/kenken.png new file mode 100644 index 00000000..dcbf8545 Binary files /dev/null and b/public/assets/images/kenken.png differ diff --git a/public/assets/images/luck.png b/public/assets/images/luck.png new file mode 100644 index 00000000..da6bac74 Binary files /dev/null and b/public/assets/images/luck.png differ diff --git a/public/assets/images/rps.png b/public/assets/images/rps.png new file mode 100644 index 00000000..f97ca319 Binary files /dev/null and b/public/assets/images/rps.png differ diff --git a/public/assets/images/sudoku.png b/public/assets/images/sudoku.png new file mode 100644 index 00000000..bb1e4e38 Binary files /dev/null and b/public/assets/images/sudoku.png differ diff --git a/public/assets/images/tower-defence.png b/public/assets/images/tower-defence.png new file mode 100644 index 00000000..f9ff674f Binary files /dev/null and b/public/assets/images/tower-defence.png differ diff --git a/public/assets/luck/pot-of-gold.svg b/public/assets/luck/pot-of-gold.svg new file mode 100644 index 00000000..42db124e --- /dev/null +++ b/public/assets/luck/pot-of-gold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/rockpaperscissors/paper.png b/public/assets/rockpaperscissors/paper.png new file mode 100644 index 00000000..38604595 Binary files /dev/null and b/public/assets/rockpaperscissors/paper.png differ diff --git a/public/assets/rockpaperscissors/rock.png b/public/assets/rockpaperscissors/rock.png new file mode 100644 index 00000000..f6ec859c Binary files /dev/null and b/public/assets/rockpaperscissors/rock.png differ diff --git a/public/assets/rockpaperscissors/rps.png b/public/assets/rockpaperscissors/rps.png new file mode 100644 index 00000000..cc9c40d9 Binary files /dev/null and b/public/assets/rockpaperscissors/rps.png differ diff --git a/public/assets/rockpaperscissors/scissors.png b/public/assets/rockpaperscissors/scissors.png new file mode 100644 index 00000000..1ad1668b Binary files /dev/null and b/public/assets/rockpaperscissors/scissors.png differ diff --git a/public/assets/tower-defence/brick1.svg b/public/assets/tower-defence/brick1.svg new file mode 100644 index 00000000..5b4f0950 --- /dev/null +++ b/public/assets/tower-defence/brick1.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/tower-defence/cannon.svg b/public/assets/tower-defence/cannon.svg new file mode 100644 index 00000000..261711b9 --- /dev/null +++ b/public/assets/tower-defence/cannon.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + Layer 1 + + + + + \ No newline at end of file diff --git a/public/assets/tower-defence/machine-gun.svg b/public/assets/tower-defence/machine-gun.svg new file mode 100644 index 00000000..6f21ef5f --- /dev/null +++ b/public/assets/tower-defence/machine-gun.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + Layer 1 + + + + + \ No newline at end of file diff --git a/public/assets/tower-defence/pistol-gun.svg b/public/assets/tower-defence/pistol-gun.svg new file mode 100644 index 00000000..eb2d45ec --- /dev/null +++ b/public/assets/tower-defence/pistol-gun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/tower-defence/sand.svg b/public/assets/tower-defence/sand.svg new file mode 100644 index 00000000..ed0e26fa --- /dev/null +++ b/public/assets/tower-defence/sand.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + Layer 1 + + + \ No newline at end of file diff --git a/public/assets/tower-defence/station.svg b/public/assets/tower-defence/station.svg new file mode 100644 index 00000000..6960d886 --- /dev/null +++ b/public/assets/tower-defence/station.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/tower-defence/truck3b_body.png b/public/assets/tower-defence/truck3b_body.png new file mode 100644 index 00000000..5b4bbd9d Binary files /dev/null and b/public/assets/tower-defence/truck3b_body.png differ diff --git a/public/css/ex-game/ex-game.css b/public/css/ex-game/ex-game.css new file mode 100644 index 00000000..e69de29b diff --git a/public/css/kenken/kenken.css b/public/css/kenken/kenken.css new file mode 100644 index 00000000..55fc690d --- /dev/null +++ b/public/css/kenken/kenken.css @@ -0,0 +1,74 @@ +table{ + border: 1px solid black; +} + +td{ + border: 2px solid grey; + text-align: center; + vertical-align: middle; + width: 48px; + height: 48px; +} + +#game-area input{ + color: black; + padding: 0; + border: 0; + text-align: center; + width: 92px; + height: 92px; + font-size: 46px; + outline: none; +} + +h1,h3,h4{ + display: inline-block; +} + +h4{ + cursor: pointer; +} + +#win{ + color: green; + display: none; +} + +#timer{ + float: right; + margin-top: 10px; +} + +#playAgain{ + float: right; + display: none; +} + +.col-sp{ + width: 480px; + /*margin: auto;*/ +} + +.modal-dialog{ + margin-top: 100px!important; +} + +.down,.bottom{ + border-bottom: 6px solid black; +} + +.up,.top{ + border-top: 6px solid black; +} + +.right{ + border-right: 6px solid black; +} + +.left{ + border-left: 6px solid black; +} + +td span{ + top:0;left:5px; position:absolute; +} \ No newline at end of file diff --git a/public/css/rockpaperscissors/rps.css b/public/css/rockpaperscissors/rps.css new file mode 100644 index 00000000..83367a89 --- /dev/null +++ b/public/css/rockpaperscissors/rps.css @@ -0,0 +1,80 @@ +h1,h3{ + display: inline-block; +} + +#wins{ + float: right; + margin-top: 5px; +} + +#game-area{ + display: flex; + flex-direction: column; + justify-content: center; +} + +#prompt{ + display: flex; + justify-content: center; +} + +#options{ + display: flex; + justify-content: center; + align-items: center; +} + +.option{ + width: 150px; + height: 150px; + cursor: pointer; +} + +.result{ + width: 150px; + height: 150px; +} + +#nextRound{ + display: flex; + justify-content: center; + margin-top: 30px; + align-items: center; +} + +#finalResults{ + display: flex; + justify-content: center; + margin-top: 30px; + align-items: center; +} + +#finalResults .result{ + width: 50px; + height: 50px; +} + +#finalResults2{ + display: flex; + justify-content: center; + margin-top: 30px; + align-items: center; +} + +#finalResults2 .result{ + width: 50px; + height: 50px; +} + +#rounds{ + float:right; + margin-right: 30px; + margin-top: 5px; +} + +#playAgain{ + display: flex; + justify-content: center; + margin-top: 30px; + align-items: center; +} \ No newline at end of file diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 00000000..1906473a --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,78 @@ + +/* NAV */ + +nav { + background-color: #0055aa; +} + + +nav button, nav a, nav span { + color: #fff!important; +} + +#signInBtn { + background-color: #0055aa; +} + +nav .dropdown-menu { + /* make dropdown background match button color */ + background-color: #1d2124; +} + + +/* GAME BOX */ + +.gameBox { + display: inline-block; + overflow: hidden; + width: 200px; + height: 150px; + text-align: center; +} + +.gameBox img { + width: 100%; + height: 100%; + min-height: 150px; +} + +.gameMeta { + display: inline-block; + position: relative; + width: 100%; + height: 100%; + background: rgb(220, 225, 225); /* Fallback color */ + background: rgba(220, 225, 225, 0.8); /* Black background with 0.5 opacity */ + color: black; + padding: 0.25em 1em 0.25em; + -moz-transition: all 0.4s ease-in-out 0s; + -webkit-transition: all 0.4s ease-in-out 0s; + -ms-transition: all 0.4s ease-in-out 0s; + transition: all 0.4s ease-in-out 0s; + -moz-transform: translateY(-2.5em); + -webkit-transform: translateY(-2.5em); + -ms-transform: translateY(-2.5em); + transform: translateY(-2.5em); + text-decoration: none!important; +} + +.gameBox:hover .gameMeta { + -moz-transform: translateY(-100%); + -webkit-transform: translateY(-100%); + -ms-transform: translateY(-100%); + transform: translateY(-100%); +} + +.gameMeta h3 { + margin: 0; +} + +.gameMeta p { + margin: .5em 0 0; +} + +/* score board */ +#scoreModal .tab-content{ + overflow-y: scroll; + max-height: 50vh; +} diff --git a/public/css/sudoku/sudoku.css b/public/css/sudoku/sudoku.css new file mode 100644 index 00000000..8117ec12 --- /dev/null +++ b/public/css/sudoku/sudoku.css @@ -0,0 +1,63 @@ +table{ + border: 1px solid black; +} + +td{ + border: 2px solid black; + text-align: center; + vertical-align: middle; + width: 48px; + height: 48px; +} + +input{ + color: black; + padding: 0; + border: 0; + text-align: center; + width: 48px; + height: 48px; + font-size: 24px; + outline: none; +} + +h1,h3,h4{ + display: inline-block; +} + +h4{ + cursor: pointer; +} + +#win{ + color: green; + display: none; +} + +#timer{ + float: right; + margin-top: 10px; +} + +#playAgain{ + float: right; + display: none; +} + +.col-sp{ + width: 480px; + /*margin: auto;*/ +} + +.modal-dialog{ + margin-top: 100px!important; +} + +#cell-1,#cell-2,#cell-3,#cell-4,#cell-5,#cell-6,#cell-7,#cell-8,#cell-9{border-top: 4px solid black} +#cell-28,#cell-29,#cell-30,#cell-31,#cell-32,#cell-33,#cell-34,#cell-35,#cell-36{border-top: 4px solid black} +#cell-55,#cell-56,#cell-57,#cell-58,#cell-59,#cell-60,#cell-61,#cell-62,#cell-63{border-top: 4px solid black} +#cell-3,#cell-12,#cell-21,#cell-30,#cell-39,#cell-48,#cell-57,#cell-66,#cell-75{border-right: 4px solid black} +#cell-6,#cell-15,#cell-24,#cell-33,#cell-42,#cell-51,#cell-60,#cell-69,#cell-78{border-right: 4px solid black} +#cell-9,#cell-18,#cell-27,#cell-36,#cell-45,#cell-54,#cell-63,#cell-72,#cell-81{border-right: 4px solid black} +#cell-73,#cell-74,#cell-75,#cell-76,#cell-77,#cell-78,#cell-79,#cell-80,#cell-81{border-bottom: 4px solid black} +#cell-1,#cell-10,#cell-19,#cell-28,#cell-37,#cell-46,#cell-55,#cell-64,#cell-73{border-left: 4px solid black} \ No newline at end of file diff --git a/public/js/auth-forms.js b/public/js/auth-forms.js new file mode 100644 index 00000000..a5630155 --- /dev/null +++ b/public/js/auth-forms.js @@ -0,0 +1,323 @@ +// TODO get import to work +// import * as firebase from "firebase-admin"; +let remoteHandel; // store callback to forum submit here for when user is not logged in. +const handelRemoteHandel = () => { + if (remoteHandel) { + remoteHandel(); + remoteHandel = undefined; + } +}; +/** + * Parse the form data. Handel parsing errors with warning messages and aborting the post. + * Post the form data to the given url. Then call with handelResponse with the response data. + * @author: jk + * @param {Event} e The submit button press event. + * @param {string} url The url to submit the post to. + * @param {function} dataParser The function to parse the sign in form. + * @param {function} handelResponse The function to handel the data from a sign in. + * @returns {boolean} Always returns false. + */ +function submit(e, url, dataParser, handelResponse) { + console.log(e, url, dataParser, handelResponse); + e.preventDefault(); // prevent url form submission + + const jsonData = dataParser(); + const data = JSON.stringify(jsonData); + console.log("form data: ", data); + + if (jsonData.warningStatus === "failed") { + const warning = e.target.parentElement.parentElement.querySelector("div#warningAlerts span"); + warning.textContent = jsonData.warningMsg; + warning.parentElement.classList.remove("d-none"); + return false; + } + + let request = new Request(url, { + method: "POST", + body: data, + headers: { + "Content-Type": "application/json", + }, + }); + + fetch(request) + .then((resp) => resp.json()) + .then(function( data ) { + // do something with the response + console.log( data ); + handelResponse(data); + }) + .catch(function (error) { + console.log( error ); + }); + console.log("done submitting"); + return false; +} + +/** + * Handel the sign in submit button event. + * @author: jk + * @param {Event} e The submit button press event. + * @param {function} dataParser The function to parse the sign in form. + * @param {function} handelResponse The function to handel the data from a sign in. + */ +function signInSubmit(e, dataParser, handelResponse) { + console.log(e, dataParser, handelResponse); + e.preventDefault(); // prevent url form submission + handelResponse(dataParser()); +} + +/** + * Attach onclick events for form submit buttons. + * @author: jk + * @returns {Promise} + */ +addLoadEvent(async function() { + const submitAuthSignUpUrl = "/auth/sign-up"; + // bind sign in and sign up events + document.getElementById("signInSubmitBtn").onclick = ((e) => signInSubmit(e, parseSignInForm, handelSignInResponse)); + document.getElementById("signUpSubmitBtn").onclick = ((e) => submit(e, submitAuthSignUpUrl, parseSignUpForm, handelSignUpResponse)); +}); + + +// CLOSE MODAL // + +/** + * Close the given modal. This will close all modal backdrops. + * @author: jk + * @param modal The modal to close. + */ +function closeModal(modal) { + modal.querySelector(".modal-header > button.close").click(); + modal.querySelector("form").reset(); + modal.querySelector(".alert").classList.remove("d-none") +} + + +// PARSE FORM // +/** + * Pull the data out of the sign in form. + * @author: jk + * @returns {{password: *, email: *}} + */ +function parseSignInForm() { + return { + email: document.getElementById("sign-in-email").value, + password: document.getElementById("sign-in-password").value, + } +} + +/** + * Pull the data out of the sign up form. + * @author: jk + * @returns {{password: *, name: *, email: *}} + */ +function parseSignUpForm() { + return { + name: document.getElementById("sign-up-name").value, + email: document.getElementById("sign-up-email").value, + password: document.getElementById("sign-up-password").value, + } +} + + +// HANDEL RESPONSE // + +// sign ins are done client side +/** + * Sign in with the provided email and password using the firebase api. + * @author: jk + * @param data Sign in forum data. + * @returns {Promise} + */ +async function handelSignInResponse(data) { + firebaseSignInEmailPassword(data.email, data.password) + .then(function() { + const modal = document.getElementById("signInFormModal"); + closeModal(modal); + handelRemoteHandel(); + console.log("you are now logged in"); + }) + .catch(function(error) { + console.log("sign in failed", error); + document.querySelector("#sign-in-alert > span").textContent = error.message; + document.getElementById("sign-in-alert").classList.remove("d-none"); + }); +} + +/** + * On a successful sign up, use the token to sign in. If the sign up failed, show the failure message on the form. + * @author: jk + * @param data The data from the sign up response in the backend. + * @returns {Promise} + */ +async function handelSignUpResponse(data) { + if (await firebaseSignInToken(data.customToken) === true) { + const modal = document.getElementById("signUpFormModal"); + closeModal(modal); + handelRemoteHandel(); + console.log("you are now logged in"); + } else { + console.log("sign up failed"); + console.log(data.failMsg); + if (data.failMsg) { + document.querySelector("#sign-up-alert > span").textContent = data.failMsg; + document.getElementById("sign-up-alert").classList.remove("d-none"); + } + } +} + + +// FIREBASE AUTH // + +const auth = firebase.auth(); + +/** + * Sign in using the provided custom token. + * @author: jk + * @param token The token for the user authentication. + * @returns {Promise} True is success; false if failed. + */ +async function firebaseSignInToken(token) { + if (typeof token !== "string") { + // if token is bad dont bother trying to sign in + return false; + } + return await auth.signInWithCustomToken(token) + .then(function() { + return true; + }) + .catch(function(error) { + // Handle Errors here. + const errorCode = error.code; + const errorMessage = error.message; + return false; + }); +} + +/** + * Sign in using the firebase api for email and password authentication. + * @author: jk + * @param email + * @param password + * @returns {Promise} + */ +async function firebaseSignInEmailPassword(email, password) { + if (typeof email !== "string" || typeof password !== "string") { + // if creds are bad dont bother trying to sign in + return false; + } + return auth.signInWithEmailAndPassword(email, password); +} + +/** + * Sign out using the firebase api. + * @author: jk + * @returns {Promise} True when successful; false when not. + */ +async function firebaseSignOut() { + return await auth.signOut() + .then(function() { + console.log("User signed out"); + return true; + }) + .catch(function(error) { + console.log("User sign out failed", error); + return false; + }); +} + +// DISPLAY USER // + +firebase.auth().onAuthStateChanged(update); + +/** + * Update the ui with relevant auth data: auth buttons and nav welcome message. + * @author: jk + * @param {UserRecord} user The current user signed in. + */ +function update(user) { + if (user === null || user === undefined) { + user = auth.currentUser; + } + updateNavButtons(user); + updateNavName(user); +} + +/** + * Show the log out button if logged in. + * Show the sign up and log in button if not logged in. + * @author: jk + * @param {UserRecord} user The current user signed in. + */ +function updateNavButtons(user) { + if (user === null || user === undefined) { + document.getElementById("signUpBtn").classList.remove("d-none"); + document.getElementById("signInMenuDiv").classList.remove("d-none"); + document.getElementById("signOutBtn").classList.add("d-none"); + } else { + document.getElementById("signUpBtn").classList.add("d-none"); + document.getElementById("signInMenuDiv").classList.add("d-none"); + document.getElementById("signOutBtn").classList.remove("d-none"); + } +} + +/** + * add the welcome message to the nav bar + * @author: jk + * @param {UserRecord} user The current user signed in. + */ +function updateNavName(user) { + document.querySelector("nav> #userDisplayName").textContent = (user === null || user === undefined) ? "" : "Welcome " + user.displayName; +} + + +// OAuth // + +/** + * Handel a google oAuth sign in using the firebase auth api + * @author: jk + */ +function googleSignIn() { + const provider = new firebase.auth.GoogleAuthProvider(); + // provider.addScope("https://www.googleapis.com/auth/contacts.readonly"); + provider.addScope("profile"); + provider.addScope("email"); + auth.signInWithPopup(provider).then(function(result) { + // This gives you a Google Access Token. You can use it to access the Google API. + const token = result.credential.accessToken; + // The signed-in user info. + const user = result.user; + handelRemoteHandel(); + // console.log("Google user signed in ", token, result.user) + }).catch(function(error) { + if (error.code === "auth/account-exists-with-different-credential") { + alert("You have signed up with a different provider for that email."); + // Handle linking here if your app allows it. + } else { + console.log("Google sign in error", error) + } + }); +} + +/** + * Handel a github oAuth sign in using the firebase auth api + * @author: jk + */ +function githubSignIn() { + const provider = new firebase.auth.GithubAuthProvider(); + provider.addScope("read:user"); + provider.addScope("user:email"); + auth.signInWithPopup(provider).then(function(result) { + handelRemoteHandel(); + // console.log("Github user signed in ", result.credential.accessToken, result.user) + }).catch(function(error) { + if (error.code === "auth/account-exists-with-different-credential") { + alert("You have signed up with a different provider for that email."); + // Handle linking here if your app allows it. + } else { + console.error("Github sign in error", error); + } + }); + +} diff --git a/public/js/bdest/main.js b/public/js/bdest/main.js new file mode 100644 index 00000000..cd658eee --- /dev/null +++ b/public/js/bdest/main.js @@ -0,0 +1,306 @@ +"use strict"; + + +/* Value to configurable information about the current board */ +let board = { + initialize: function () { + this.score = 0; // Current score + this.level = 1; // Current level + this.lives = 5; // Current lives + this.active = false; // Whether the ball is in play + this.ballVelY = -500; // Constant starting velocity Y + this.ballVelX = -200; // Constant starting velocity X + this.paddleSpeed = 300; // Constant paddle speed. + this.barXScale = 0.225; // X scale for bars + this.barYScale = 0.5; // Y scale for bars + this.bricksLeft = 1; + // Current board configuration + this.rowConfiguration = ["r_bar", "r_bar", "o_bar", "o_bar", "y_bar"]; + this.over = false; // means the game is over + } +}; + +let config = { + type: Phaser.AUTO, + width: 600, + height: 800, + parent: "game-area", physics: { default: 'arcade', + arcade: { + gravity: { y: 300 }, + debug: false + } + }, + scene: { + preload: preload, + create: create, + update: update + } +}; + +let game = new Phaser.Game(config); + +const path = "/assets/bdest/"; +let input; +let ball; +let paddle; +let bricks = []; +let cursors; +let spaceBar; +let scoreText; +let livesText; +let gameOverText; +let startingText; + +function preload() { + this.load.image("ball", path + "ball.png"); + this.load.image("r_bar", path + "bar_red.png"); + this.load.image("o_bar", path + "bar_orange.png"); + this.load.image("y_bar", path + "bar_yellow.png"); + this.load.image("paddle", path + "paddle.png"); +} + +function create() { + board.initialize(); + + // Keyboard Controls; + input = this.input; + + // Add paddle to game + paddle = this.physics.add.image(300, 750, "paddle").setScale(0.5); + paddle.setCollideWorldBounds(true); + paddle.body.setAllowGravity(false); + + // Add ball to game + ball = this.physics.add.image(paddle.x, 700 - 23 * 0.5, "ball").setScale(0.075); + ball.setCollideWorldBounds(true); + ball.body.setAllowGravity(false); + ball.setBounce(1, 1); + ball.setVisible(false); + + // Add collision + this.physics.add.collider(ball, paddle, collisionWithBall, null); + + // Keyboard controls + cursors = this.input.keyboard.createCursorKeys(); + spaceBar = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE); + + // HARD BINDING !!!! + let initializeBricksArrayBound = initializeBricksArray.bind(this); + + // Get bricks onto the field + initializeBricksArrayBound(bricks, board.barXScale, board.barYScale, board.rowConfiguration); + board.bricksLeft = bricks.length; + + // Add score HUD + scoreText = this.add.text(16, 16, 'Score: ' + board.score, { fontSize: '32px', fill: '#fff' }); + livesText = this.add.text(400, 16, 'Lives: ' + board.lives, { fontSize: '32px', fill: '#fff' }); + gameOverText = this.add.text(300, 400, 'GAME OVER', { fontSize: '64px', fill: '#fff' }); + gameOverText.setVisible(false); + gameOverText.setOrigin(0.5); + startingText = this.add.text(300, 600, 'Press the space bar to serve!', { fontSize: '32px', fill: '#fff' }); + startingText.setOrigin(0.5); +} + +function update() { + let paddleActions = onKeyPress.bind(this); + paddleActions(); + + if (board.lives === 0 && !board.over) { + board.over = true; + gameOverText.setVisible(true); + startingText.setVisible(true); + submitScore(); + } + + if (ball !== undefined) { + if (ball.y > 780) { + // Update lives counter + board.lives -= 1; + livesText.setText("Lives: " + board.lives); + // Make ball stop and invisible + ball.setVisible(false); + ball.setY(700); + ball.setVelocityY(0); + ball.setVelocityX(0); + board.active = false; + } else if (board.bricksLeft === 0) { + // Make ball stop and invisible + ball.setVisible(false); + ball.setY(700); + ball.setVelocityY(0); + ball.setVelocityX(0); + + // Reset all of the bricks + resetBricks(bricks); + board.active = false; + } + } +} + +/*** + * Re-enables the current ball. + */ +const addBall = function () { + ball.setVisible(true); + ball.setImmovable(false); + ball.setX(paddle.x); + ball.setY(750 - 23 * 0.5); + ball.setVelocityX(board.ballVelX); + ball.setVelocityY(board.ballVelY); +} + +/*** + * Function to handle input events. + */ +const onKeyPress = function () { + + /* Movement */ + if (cursors.left.isDown) { + /* Enable the paddle to move when a key is pressed */ + paddle.setImmovable(false); + paddle.setVelocityX(-board.paddleSpeed); + /* Disable movement to prevent collision */ + paddle.setImmovable(); + } else if (cursors.right.isDown) { + paddle.setImmovable(false); + paddle.setVelocityX(board.paddleSpeed); + paddle.setImmovable(); + /* Only send ball if board is not active (i.e. life is not lost or game over) */ + } else if (spaceBar.isDown && !board.active && board.lives > 0) { + startingText.setVisible(false); + let addBallBound = addBall.bind(this); + addBallBound(); + board.active = true; + } else if (spaceBar.isDown && board.lives === 0) { + startingText.setVisible(false); + resetBoard(); + let addBallBound = addBall.bind(this); + addBallBound(); + } + else { + paddle.setVelocityX(0); + } +} + +/*** + * Function to initialize the array of bricks that store the information of + * each brick on the board. + * @param {Array} bricks - empty array of bricks + * @param {Number} brickX - X scale of the bricks + * @param {Number} brickY - Y scale of the bricks + * @param {Array} rows - The array of brick types specified by its color + */ +const initializeBricksArray = function (bricks, brickX, brickY, rows) { + + let currentY = 100; + for (let i = 0; i < rows.length; i++) { + let currentX = 14 + (320 * brickX / 2); + for (let j = 0; j < 8; j++) { + let currBrickWrapper = new Brick(bricks, rows[i]); + let brickObj = this.physics.add.image(currentX, currentY, rows[i]).setScale(brickX, brickY); + brickObj.body.setAllowGravity(false); + brickObj.setImmovable(); + // Add collision + this.physics.add.collider(ball, brickObj, barCollision, null); + brickObj.brick = currBrickWrapper; + bricks.push(brickObj); + currentX += 320 * brickX; + } + + currentY += 100 * brickY; + } +} + +/** + * Brick object constructor + * @param {Array} bricks - array of bricks + * @param {String} color - type of bar + */ +function Brick(bricks, color) { + /* Set score based on color */ + if (color === "r_bar") { + this.score = 200; + } else if (color === "o_bar") { + this.score = 100; + } else if (color === "y_bar") { + this.score = 50; + } +} + +/*** + * Callback to handle the collision between the ball and paddle. + * @param {Phaser.GameObjects.GameObject} ball - The ball + * @param {Phaser.GameObjects.GameObject} paddle - The paddle + */ +function collisionWithBall(ball, paddle) { + /* The paddle should stay where it is */ + let currentVelocity = Math.sqrt(board.ballVelX ** 2 + board.ballVelY ** 2); + /* Get the angle from the center of the center of the paddle to the ball */ + let reflectionAngle = Math.atan(Math.abs(ball.y - paddle.y) / Math.abs(paddle.x - ball.x)); + + let Xreflect = (paddle.x - ball.x < 0) ? 1 : -1; + + /* Set a default velocity from 0.75 to 1.25 from current */ + const rand = Math.random() * 50; + currentVelocity = (rand > 25) ? currentVelocity * 1.25 : currentVelocity * 0.75; + + ball.setVelocityX(Xreflect * currentVelocity * Math.cos(reflectionAngle)); + ball.setVelocityY(-currentVelocity * Math.sin(reflectionAngle)); +} + +/*** + * Callback to handle collisions. + */ +function barCollision(ball, brick) { + board.score += brick.brick.score; + scoreText.setText("Score: " + board.score); + board.bricksLeft--; + brick.disableBody(true, true); +} + + +/*** + * Function to reset the board after a game over. + */ +function resetBoard() { + board.active = true; + board.over = false; + + resetBricks(bricks); + + // Reset lives counter + board.lives = 5; + livesText.setText("Lives: " + board.lives); + + // Reset score counter + board.score = 0; + scoreText.setText("Score: " + board.score); + + // Make the game over text disappear + gameOverText.setVisible(false); +} + + +/*** + * Submit the final score from the user + */ +function submitScore() { + let scoreModel = $("#gameScoreFormModal"); + attachHeading(`Score: ${board.score}`); + scoreModel.on("hidden.bs.modal", ()=>{}); + attachSubmit({ score: board.score }, () => { + scoreModel.modal("hide"); + }); + scoreModel.modal("show"); // user jQuery to show the modal +} + +/*** + * Reset all of the bricks on the board. + * @param {Array} bricks - Array of Brick objects. + */ +function resetBricks(bricks) { + for (let i = 0; i < bricks.length; i++) { + bricks[i].enableBody(true, bricks[i].x, bricks[i].y, true, true); + } + board.bricksLeft = bricks.length; +} \ No newline at end of file diff --git a/public/js/ex-game/ex-game.js b/public/js/ex-game/ex-game.js new file mode 100644 index 00000000..08694fb5 --- /dev/null +++ b/public/js/ex-game/ex-game.js @@ -0,0 +1,22 @@ +function validate(e){ + var theEvent = e || window.e; + // Handle key press + var key = theEvent.keyCode || theEvent.which; + key = String.fromCharCode(key); + var regex = /[0-9]|\./; + if( !regex.test(key) ) { + theEvent.returnValue = false; + if(theEvent.preventDefault) theEvent.preventDefault(); + } +} + +function giveScore(){ + let score = document.getElementById('submitvalue').value; + const scoreModel = $("#gameScoreFormModal"); + attachHeading(`Score: ${score} points`); + //scoreModel.on("hidden.bs.modal", load); + attachSubmit({score: score}, () => { + scoreModel.modal("hide"); + }); + scoreModel.modal("show"); // user jQuery to show the modal +} \ No newline at end of file diff --git a/public/js/flappygoat/main.js b/public/js/flappygoat/main.js new file mode 100644 index 00000000..158a1653 --- /dev/null +++ b/public/js/flappygoat/main.js @@ -0,0 +1,245 @@ + +const FPS = 30; +const GOAT_SIZE = 80; // height in pixels +const TEXT_SIZE = 40; // text font height in pixels +const DEBUG_MODE = false; // develop debug mode + +/** @type {HTMLCanvasElement} */ +let canv = document.getElementById("gameCanvas"); +let ctx = canv.getContext("2d"); +let goatImg = new Image(); +let skyImg = new Image(); +goatImg.src = "/assets/flappyGoat/flappyGoat.png"; +skyImg.src = "/assets/flappyGoat/bluesky.jpg"; + + + +// set up game params +let goat, pipes, score, text, go, count; +pipes = []; +newGame(); + + +// event handlers +document.addEventListener("keydown", keyDown); +document.addEventListener("keyup", keyUp); + +// set up game loop +setInterval(update, 1000 / FPS); + + +function addPipe() { + pipes.push(newPipe(canv.width)); +} + + +function destroyPipe(index) { + pipes.splice(index, 1); +} + + + +function drawGoat(x, y) { + ctx.drawImage(goatImg, x, y); +} + + +function gameOver() { + go = false; + goat.dead = true; + text = "Game Over"; + const scoreModel = $("#gameScoreFormModal"); + attachHeading(`Score: ${score} point(s)`); + //scoreModel.on("hidden.bs.modal", load); + attachSubmit({score: score}, () => { + scoreModel.modal("hide"); + }); + scoreModel.modal("show"); // user jQuery to show the modal + newGame(); +} + +function keyDown(/** @type {KeyboardEvent} */ ev){ + + if(goat.dead) { + return; + } + + if (ev.keyCode === 32) { + go = true; + jump(); + goat.yv = 0; + } +} + +function keyUp(/** @type {KeyboardEvent} */ ev) { + + if(goat.dead) { + return; + } + + if (ev.keyCode === 32) { + goat.canJump = true; + jump(); + } +} + +function newPipe(x) { + let space = Math.random() * 300 + 50; + return { + x1: x + 25, + x2: x - 25, + y1: space, + y2: space + 150, + r: 50, + points: false, + }; +} + +function newGame() { + score = 0; + count = 0; + pipes = []; + goat = newGoat(); +} + + +function newGoat() { + return { + x: canv.width / 20, + y: canv.height / 2, + r: GOAT_SIZE / 2, + yv: 0, + canJump: true, + dead: false, + jumpTime: 0, + }; +} + +function jump() { + if(goat.canJump) { + goat.jumpTime = 5; + } + + goat.canJump = false; +} + + + +function update() { + + // draw background + ctx.drawImage(skyImg, 0, 0); + + + // draw goat + if(!goat.dead) { + drawGoat(goat.x, goat.y); + } + + // stop everything if space hasn't been pressed + if(!go){ + return; + } + + // make goat fall continually + goat.y += (1 + goat.yv); + goat.yv += 0.05; + count++; + + if(count % 75 === 0) { + addPipe(); + } + + + + // draw the pipes + let x1, x2, y1, y2; + for(let i = 0; i < pipes.length; i++) { + + // get pipe properties + x1 = pipes[i].x1; + x2 = pipes[i].x2; + y1 = pipes[i].y1; + y2 = pipes[i].y2; + + + ctx.fillStyle = "green"; + + // draw upper pipe + ctx.fillRect(x1, 0, 50, y1); + + // draw lower pipe + ctx.fillRect(x1, y2, 50, canv.height - y2); + + + } + + + // draw the score + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + ctx.fillStyle = "white"; + ctx.font = TEXT_SIZE + "px dejavu sans mono"; + ctx.fillText(score, canv.width - 10, 40); + + // test dots + if(DEBUG_MODE) { + ctx.fillStyle = "red"; + ctx.fillRect(goat.x + 30, goat.y + 10, 2, 2); + ctx.fillStyle = "blue"; + ctx.fillRect(goat.x + 80, goat.y + 70, 2, 2); + } + + // check for pipe collision + if(!goat.dead) { + for (let i = 0; i < pipes.length; i++) { + //console.log(goat.x >= pipes[i].x1); + if ((goat.x + 30) >= pipes[i].x1 && (goat.x + 30) <= (pipes[i].x1 + 50) && ((goat.y + 10) <= pipes[i].y1 || (goat.y + 70) >= pipes[i].y2)) { + gameOver(); + break; + } + if ((goat.x + 80) >= pipes[i].x1 && (goat.x + 80) <= (pipes[i].x1 + 50) && ((goat.y + 10) <= pipes[i].y1 || (goat.y + 70) >= pipes[i].y2)) { + gameOver(); + break; + } + + } + } + + + // jump goat + if(goat.jumpTime > 0 && !goat.dead) { + goat.y -= 15; + goat.jumpTime--; + } + + + // add score for crossing pipe + for(let i = 0; i < pipes.length; i++) { + if(goat.x === pipes[i].x1 && !pipes[i].points) { + score++; + pipes[i].points = true; + } + } + + // kill goat if touches bottom or top + if(goat.y + goat.r < 0 || goat.y + goat.r > canv.height) { + gameOver(); + } + + + + + // move the pipes + for(let i = 0; i < pipes.length; i++) { + pipes[i].x1 -= 5; + + //handle edge + if (pipes[i].x1 < 0 - pipes[i].r) { + destroyPipe(i); + } + } + + + +} \ No newline at end of file diff --git a/public/js/game-forms.js b/public/js/game-forms.js new file mode 100644 index 00000000..416b450e --- /dev/null +++ b/public/js/game-forms.js @@ -0,0 +1,64 @@ +/** + * Submit the game data to the backend so it can be saved to the database. + * @author: jk + * @param {Event} e The submit button press event. + * @param {Object} gameData The game data to submit. + * @param {function} handelResponse The callback for when the fetch is done. + */ +function submitGameData(e, gameData, handelResponse) { + if (auth.currentUser == null) { + console.log("no user!"); + remoteHandel = () => {$("#gameScoreFormModal").modal("show")}; // should get called after successful sign in + document.querySelector("#game-submit-alert > span").textContent = "Please sign in to submit your score."; + document.getElementById("game-submit-alert").classList.remove("d-none"); + return false; + } else { + document.querySelector("#game-submit-alert > span").textContent = ""; + document.getElementById("game-submit-alert").classList.add("d-none"); + } + const url = "/data/games/" + CURGAME.gameId; + const data = JSON.stringify({userId: auth.currentUser.uid, ...gameData}); + console.log("Game data: ", data); + + const request = new Request(url, { + method: "POST", + body: data, + headers: { + "Content-Type": "application/json", + }, + }); + + console.log(request); + + fetch(request) + .then((resp) => resp.json()) + .then(function( data ) { + // do something with the response + console.log( data ); + handelResponse(data); + }) + .catch(function (error) { + console.log( error ); + }); + console.log("Done submitting game data"); + return false; +} + +/** + * Add a string to the h3 in the forum. + * @author: jk + * @param text The string to add. + */ +function attachHeading(text) { + document.querySelector("#gameScoreFormModal h3").textContent = text; +} + +/** + * Attach the onclick event with the given data and callback. + * @author: jk + * @param data The game data to submit. + * @param handelResponse The callback for after the submit is completed. + */ +function attachSubmit(data, handelResponse) { + document.getElementById("gameScoreSubmitBtn").onclick = e => submitGameData(e, data, handelResponse); +} diff --git a/public/js/gompei/main.js b/public/js/gompei/main.js new file mode 100644 index 00000000..ff9cddf9 --- /dev/null +++ b/public/js/gompei/main.js @@ -0,0 +1,315 @@ +var config = { + type: Phaser.AUTO, + width: 800, + height: 600, + physics: { + default: "arcade", + arcade: { + gravity: { y: 500 }, + debug: false + } + }, + parent: "game-area", + scene: { + preload: preload, + create: create, + update: update + } +}; + + +const PLAY_STATE = 0; +const LOST_STATE = 1; +const PAUSE_STATE = 2; +const START_STATE = 3; +const path = "/assets/gompei/"; + +let state = START_STATE; // current state +let newState = false; // used to swap between play and pause state + +let game = new Phaser.Game(config); +let player; +let rock = []; +let grass; +let groundGroup; + +// inputs +let cursors; +let pauseKey; +let spaceBar; + +let score = 0; +let maxRocks = 3; // max number of rocks +let rockSpeed = -200; // starting speed of rocks +let rockAcc = 0.05; // acceleration of rock + +// text that appears on screen +let scoreText; +let pauseText; +let startText; + +let playerYVelocity; // current Y velocity +let extraJumps = 1; // number of extra jumps in midair +let jumpsLeft = extraJumps; // number of jumps user can still do in midair +let canJumpAgain = true; // make sure user lets go of up key before hitting it again + + +/** + * Preload images + * @author: am + */ +function preload () { + this.load.image("gompei", path + "gompei.png"); // Source: https://depositphotos.com/273483776/stock-illustration-pixel-art-goat-character-isolated.html + this.load.image("ground", path + "groundLong.png"); + this.load.image("rock", path + "rock.png"); // Source: http://pixelartmaker.com/art/da268f06e621b21 + this.load.image("grass", path + "grassLong.png"); // Source: https://imgbin.com/png/XmJBdYii/pixel-art-game-sprite-png +} + +/** + * Set up images on page with correct properties + * @author: am + */ +function create () { + + // Set ground properties (separate from grass since grass has no physics) + groundGroup = this.physics.add.staticGroup(); + groundGroup.create(0, 560, "ground"); + + // Set moving ground properties + grass = this.add.tileSprite(0,560, this.width, this.height, "grass"); + + // set player properties + player = this.physics.add.sprite(50, 450, "gompei").setScale(0.7); + this.physics.add.collider(player, groundGroup); + player.setCollideWorldBounds(true); + playerYVelocity = 0; + + // set rock properties + let offset = 700; + for (let i = 0; i < maxRocks; i++) { + rock[i] = this.physics.add.image(offset, 450, "rock").setScale(0.25); + offset += 450; + this.physics.add.collider(player, rock[i], rockCollision, null); + rock[i].setVelocityX(rockSpeed); + this.physics.add.collider(rock[i], groundGroup); + rock[i].body.allowGravity = false; + } + + // Set up inputs + cursors = this.input.keyboard.createCursorKeys(); + pauseKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.P); + spaceBar = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE); + + // Add score HUD + scoreText = this.add.text(16, 16, 'Score: 0', { fontSize: '32px', fill: '#fff' }); + + // Set up pause message + pauseText = this.add.text(this.cameras.main.centerX, this.cameras.main.centerY, "PAUSED (\'P\' to resume)", { fontSize: '28px', fill: '#fff' }); + pauseText.setX(this.cameras.main.centerX - pauseText.width / 2); + pauseText.setY(this.cameras.main.centerY - pauseText.height / 2); + pauseText.visible = false; // Hide pause text + + // Set up start message + startText = this.add.text(this.cameras.main.centerX, this.cameras.main.centerY, "Press SPACE to Start!", { fontSize: '28px', fill: '#fff' }); + startText.setX(this.cameras.main.centerX - pauseText.width / 2); + startText.setY(this.cameras.main.centerY - pauseText.height / 2); + + // Stop everything at start + stopPlayer(); + stopRocks(); + +} + +/** + * Updates and re-draws images + * @author: am + */ +function update () { + + // Pause or resume game when hitting 'p' key + pauseKey.on('down', function(event){ + if (!newState) { + if (state === PLAY_STATE) { + pauseGame(); + newState = true; + } + else if (state === PAUSE_STATE) { + resumeGame(); + newState = true; + } + } + }) + + // Allow user to pause or resume after letting go of 'p' key + pauseKey.on('up', function(event){ + if (state === PLAY_STATE) { + newState = false; + } + else if (state === PAUSE_STATE) { + newState = false; + } + }) + + // Actions that happen when game is being played + if (state === PLAY_STATE) { + + // Update score + score++; + scoreText.setText("Score: " + score); + + // Set speed of ground + grass.tilePositionX -= rockSpeed / 60; + + // Steadily increase rock speed + rockSpeed-=rockAcc; + setRockSpeeds(rockSpeed); + + // save player velocity in case of pause + playerYVelocity = player.body.velocity.y; + + // Allow player to jump again when letting go of up key + if (!cursors.up.isDown) { + canJumpAgain = true; + } + + // Jump when on ground + if (cursors.up.isDown && player.body.touching.down) { + player.setVelocityY(-400); + canJumpAgain = false; + } + + // Fast fall in midair + else if (cursors.down.isDown && !player.body.touching.down) { + player.setVelocityY(player.body.velocity.y + 30); + + } + + // Double Jump + else if (cursors.up.isDown && !player.body.touching.down && jumpsLeft > 0 && canJumpAgain) { + let newSpeed = -400; + player.setVelocityY(newSpeed); + jumpsLeft--; + canJumpAgain = false; + } + + // Refresh double jump when landing on ground + else if (player.body.touching.down) { + jumpsLeft = extraJumps; + } + + // Reset rocks after going off left side of screen + for (let i = 0; i < maxRocks; i++) { + if (rock[i].x < 0) { + rock[i].x = (Math.random() * 600) + 800; + } + } + } + + // When page loads, wait for user to hit space to start game + else if (state === START_STATE){ + if (spaceBar.isDown) { + resumeGame(); + } + } +} + + +/** + * Set velocity of all rocks + * @param {number} rockSpeed New velocity for rocks + * @author: am + */ +function setRockSpeeds (rockSpeed) { + for (let i = 0; i < maxRocks; i++) { + rock[i].setVelocityX(rockSpeed); + } +} + +/** + * Put game in lost state when player collides with a rock + * @author: am + */ +function rockCollision () { + state = LOST_STATE; + + stopPlayer(); + stopRocks(); + + const scoreModel = $("#gameScoreFormModal"); + const resetGame = data => { + location.reload(); + }; + + attachHeading(`Score: ${score} points`); + scoreModel.on("hidden.bs.modal", resetGame); + attachSubmit({score: score}, () => { + scoreModel.modal("hide"); + }); + scoreModel.modal("show"); // user jQuery to show the modal +} + +/** + * Pause game: stop all sprites and show pause text + * @author: am + */ +function pauseGame () { + state = PAUSE_STATE; + stopRocks(); + stopPlayer(); + pauseText.visible = true; +} + +/** + * Stop velocities of all rocks + * @author: am + */ +function stopRocks () { + for (let i = 0; i < maxRocks; i++) { + rock[i].setVelocityX(0); + rock[i].setVelocityY(0); + } +} + +/** + * Stop velocity of player + * @author: am + */ +function stopPlayer () { + player.body.allowGravity = false; + player.setVelocityY(0); + player.setVelocityX(0); +} + +/** + * Resume movements of all sprites and hide pause text + * @author: am + */ +function resumeGame () { + state = PLAY_STATE; + resumePlayer(); + resumeRocks(); + pauseText.visible = false; + startText.visible = false; + +} + +/** + * Resume player velocities + * @author: am + */ +function resumePlayer () { + player.body.allowGravity = true; + player.setVelocityY(playerYVelocity); + player.setVelocityX(0); +} + +/** + * Resume velocities of all rocks + * @author: am + */ +function resumeRocks () { + for (let i = 0; i < maxRocks; i++) { + rock[i].setVelocityX(rockSpeed); + rock[i].setVelocityY(0); + } +} \ No newline at end of file diff --git a/public/js/kenken/kenken.js b/public/js/kenken/kenken.js new file mode 100644 index 00000000..4c720fd0 --- /dev/null +++ b/public/js/kenken/kenken.js @@ -0,0 +1,151 @@ +let done1 = [5,1,3,2,4, + 2,5,1,4,3, + 1,4,2,3,5, + 4,3,5,1,2, + 3,2,4,5,1]; +let done2 = [5,4,1,3,2, + 3,2,4,5,1, + 4,3,2,1,5, + 1,5,3,2,4, + 2,1,5,4,3]; +let done3 = [2,3,5,1,4, + 3,2,4,5,1, + 4,1,3,2,5, + 5,4,1,3,2, + 1,5,2,4,3]; + +let possibleDones = [done1,done2,done3]; + +let currentPuzzle = 0; + +/** + * Loads the game board with a random start position + */ +function load(){ + currentPuzzle = Math.floor((Math.random()*3)); + if(currentPuzzle === 0){ + loadBoard1(); + }else if(currentPuzzle === 1){ + loadBoard2(); + }else{ + loadBoard3(); + } +} + +load(); + +/** + * sets up board option 1 + */ +function loadBoard1(){ + let gameArea = document.getElementById('game-area'); + gameArea.innerHTML = ''+ + ''+ + ''+ + ''+ + ''; +} + +/** + * sets up board option 2 + */ +function loadBoard2(){ + let gameArea = document.getElementById('game-area'); + gameArea.innerHTML = '
6+
9+
3+
5
5+
11+
7+
7+
7+
3+
6+
6+
'+ + ''+ + ''+ + ''+ + ''; +} + +/** + * sets up board option 3 + */ +function loadBoard3(){ + let gameArea = document.getElementById('game-area'); + gameArea.innerHTML = '
9+
3-
5+
12+
4-
10+
3+
5
3+
7+
1
9+
'+ + ''+ + ''+ + ''+ + ''; +} + +/** + * checks if the puzzle has been solved + * + * @returns {boolean} true if complete, false if not + */ +function checkComplete(){ + for(let i=1;i<=25;i++){ + let elem = document.querySelector('#cell-'+i+' input'); + console.log(elem.value + ' '+possibleDones[currentPuzzle][i-1]); + if(parseInt(elem.value) !== possibleDones[currentPuzzle][i-1]) { + console.log('not solved'); + return false; + } + } + console.log('solved'); + stopTimer(); + document.getElementById('win').style.display='block'; + document.getElementById('playAgain').style.display='block'; + + const scoreModel = $("#gameScoreFormModal"); + attachHeading(`Time: ${time} seconds`); + //scoreModel.on("hidden.bs.modal", load); + attachSubmit({score: time}, () => { + scoreModel.modal("hide"); + }); + scoreModel.modal("show"); // user jQuery to show the modal + + return true; +} + +let time = 0; +/** + * increases the timer + */ +let seconds = setInterval(function(){ + time += 1; + + let minutes = Math.floor(time/60); + let seconds = Math.floor(time%60); + + if(seconds > 9){ + document.getElementById('timer').innerText = "Time " + minutes + ":" + seconds; + }else { + document.getElementById('timer').innerText = "Time " + minutes + ":0" + seconds; + } +},1000); + + +/** + * stops the timer + */ +function stopTimer(){ + clearTimeout(seconds); +} + +/** + * validates keyboard input, + * only allows numbers 1-5 + * + * @param {e} the keyboard event + */ +function validate(e){ + var theEvent = e || window.e; + // Handle key press + var key = theEvent.keyCode || theEvent.which; + key = String.fromCharCode(key); + var regex = /[1-5]|\./; + if( !regex.test(key) ) { + theEvent.returnValue = false; + if(theEvent.preventDefault) theEvent.preventDefault(); + } +} + +/** + * reloads the page + */ +function playAgain(){ + location.reload(); +} \ No newline at end of file diff --git a/public/js/luck/main.js b/public/js/luck/main.js new file mode 100644 index 00000000..15d1a170 --- /dev/null +++ b/public/js/luck/main.js @@ -0,0 +1,153 @@ +const config = { + type: Phaser.AUTO, + width: 800, + height: 600, + parent: "game-area", + scene: { + preload, + create, + update + } +}; + +const game = new Phaser.Game(config); +const mean = Math.pow(2, 12); +const max = mean * 2; +const min = 0; +const stddev = mean / 4; +const startReadings = 10; +let readingsLeft = startReadings; +let totalLuck = 0; + +/** + * Load game assets. + * @author: jk + */ +function preload() { + // assets from www.flaticon.com + this.load.image("potOfGold", "/assets/luck/pot-of-gold.svg"); +} + +/** + * Create the game objects. The game logic is written here though event callbacks. + * @author: jk + */ +function create() { + const scene = this; + const playBtn = scene.add.image(250, 300, "potOfGold").setInteractive(); + const startText = scene.add.text(226, 330, "Start"); + const readingsContainer = scene.add.container(5, 5); + readingsContainer.add(scene.add.text(0, 0, `Readings Left: ${readingsLeft}`, {fontSize: 28})); + readingsContainer.add(scene.add.text(0, 40, `Luck: ${getAvgLuck()}`, {fontSize: 28})); + const textContainer = scene.add.container(350, 260); + playBtn.setScale(0.2, 0.2); + playBtn.on("pointerdown", function (pointer) { + this.setTint(0xffff00); + }); + playBtn.on("pointerout", function (pointer) { + this.clearTint(); + }); + playBtn.on("pointerup", function (pointer) { + if (readingsLeft === 0) return; + this.clearTint(); + startText.visible = false; + const num = getNumber(); + const luck = getLuck(num); + textContainer.removeAll(); + textContainer.add(scene.add.text(0, 0, `Your number is ${num.toFixed(0)}!`)); + textContainer.add(scene.add.text(0, 60, `Your luck is at ${luck.toFixed(3)}%`)); + readingsLeft--; + totalLuck += luck; + readingsContainer.removeAll(); + readingsContainer.add(scene.add.text(0, 0, `Readings Left: ${readingsLeft}`, {fontSize: 28})); + readingsContainer.add(scene.add.text(0, 40, `Luck: ${getAvgLuck().toFixed(3)}%`, {fontSize: 28})); + if (readingsLeft === 0) { + const score = getAvgLuck(); + const scoreModel = $("#gameScoreFormModal"); + const resetGame = data => { + readingsLeft = startReadings; + totalLuck = 0; + textContainer.removeAll(); + readingsContainer.removeAll(); + readingsContainer.add(scene.add.text(0, 0, `Readings Left: ${readingsLeft}`, {fontSize: 28})); + readingsContainer.add(scene.add.text(0, 40, `Luck: ${getAvgLuck()}`, {fontSize: 28})); + startText.visible = true; + }; + attachHeading(`Score: ${score.toFixed(3)}%`); + scoreModel.on("hidden.bs.modal", resetGame); + attachSubmit({score: score}, () => { + scoreModel.modal("hide"); + }); + scoreModel.modal("show"); // user jQuery to show the modal + } + }); +} + +function update() { + +} + +/** + * Returns a number between min and max following a normal distibution + * @author: jk + * @returns {Number} + */ +function getNumber() { + return createMemberInNormalDistribution(mean, stddev); +} + + +/** + * Returns member of set with a given mean and standard deviation + * @author: https://stackoverflow.com/a/196941 + * @param {Number} mean + * @param {Number} std_dev + * @returns {Number} + */ +function createMemberInNormalDistribution(mean, std_dev) { + let diff = (gaussRandom() * std_dev); + let adj = mean + diff; // adjusted for range + if (adj > max) { + adj = max; + } else if (adj < min) { + adj = min; + } + return adj; +} + +/** + * Returns random number in normal distribution centering on 0. + * ~95% of numbers returned should fall between -2 and 2 + * ie within two standard deviations + * @author: https://stackoverflow.com/a/196941 + */ +function gaussRandom() { + const u = 2 * Math.random() - 1; + const v = 2 * Math.random() - 1; + const r = u * u + v * v; + /*if outside interval [0,1] start over*/ + if (r === 0 || r >= 1) return gaussRandom(); + const c = Math.sqrt(-2 * Math.log(r) / r); + return u * c; +} + +/** + * Calculate the percentage from the mean linearly. + * @param number The number generated. + * @returns {number} The luck percentage. + */ +function getLuck(number) { + return (100 * Math.abs(mean - number) / mean); +} + +/** + * Average the luck over the number of readings. + * @returns {number} The average Luck percentage. + */ +function getAvgLuck() { + if (readingsLeft !== startReadings) { + return totalLuck / (startReadings - readingsLeft); + } else { + return 0; + } +} diff --git a/public/js/rockpaperscissors/rps.js b/public/js/rockpaperscissors/rps.js new file mode 100644 index 00000000..c17dd054 --- /dev/null +++ b/public/js/rockpaperscissors/rps.js @@ -0,0 +1,161 @@ +let wins = 0; +let round = 1; +yourOptions = []; +aiOptions = []; + +/** + * Fired when the user makes a choice + * Decides who won the round and progresses + * the game + * + * @param {option} what the player + * chose: rock, paper, or scissors + */ +function makeChoice(option){ + let aiOption = aiChoice(); + yourOptions.push(option); + aiOptions.push(aiOption); + let status = ''; + if(option === 'rock'){ + if(aiOption === 'rock'){ //tie + status = 'tie'; + }else if(aiOption === 'paper'){ //lose + wins--; + status = 'lose'; + }else{ //win + wins++; + status = 'win'; + } + }else if(option === 'paper'){ + if(aiOption === 'rock'){ //win + wins++; + status = 'win'; + }else if(aiOption === 'paper'){ //tie + status = 'tie'; + }else{ //lose + wins--; + status = 'lose'; + } + }else{ + if(aiOption === 'rock'){ //lose + wins--; + status = 'lose'; + }else if(aiOption === 'paper'){ //win + wins++; + status = 'win'; + }else{ //tie + status = 'tie'; + } + } + document.getElementById('wins').innerText='Score: '+wins; + updateGameArea(option, aiOption, status); +} + +/** + * Randomly decides what the AI will choose + * + * @returns {string} rock paper or scissors + */ +function aiChoice(){ + let choice = Math.floor((Math.random()*3)); + if(choice === 0){ + return 'rock'; + }else if(choice === 1){ + return 'paper'; + }else{ + return 'scissors'; + } +} + +/** + * updates the game area to display who won a round + * + * @param {option} what you chose to play + * @param {aiOption} what the ai chose + * @param {status} if you won, lost, or drew the round + */ +function updateGameArea(option, aiOption,status){ + let response = ''; + if(status === 'win'){ + response = 'You won!'; + }else if(status === 'lose'){ + response = "You lost :'^("; + }else{ + response = "It's a tie!" + } + document.getElementById('game-area').innerHTML = '
\n' + + '

'+response+'

\n' + + '
\n' + + '
\n' + + ' ' + + '

VS

'+ + ' \n' + + '
'+ + '
'; + +} + +/** + * Changes the result back to the choice + * Also ends the game if 10 rounds have been played + */ +function nextRound(){ + round++; + if(round === 11){ + finalResults(); + + const scoreModel = $("#gameScoreFormModal"); + attachHeading(`Score: ${wins} wins`); + //scoreModel.on("hidden.bs.modal", load); + attachSubmit({score: wins}, () => { + scoreModel.modal("hide"); + }); + scoreModel.modal("show"); // user jQuery to show the modal + }else { + document.getElementById('rounds').innerText = 'Round: ' + round; + document.getElementById('game-area').innerHTML = '
\n' + + '

Choose your move!

\n' + + '
\n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + '
'; + } +} + +/** + * displays the final results + * whether you have won, lost, or drawn + * and what each player played. + * Also gives the user the option to play again. + */ +function finalResults(){ + let gameArea = document.getElementById('game-area'); + if(wins === 0){ + gameArea.innerHTML = '

You tied the computer!

'+ + '
' + + '

Your moves:

' + + '

CPU moves:

' + + '
'; + }else if(wins > 0){ + gameArea.innerHTML = '

You Won!

'+ + '
' + + '

Your moves:

' + + '

CPU moves:

' + + '
'; + }else{ + gameArea.innerHTML = '

You lost!

'+ + '
' + + '

Your moves:

' + + '

CPU moves:

' + + '
'; + } +} + +/** + * Reloads the page + */ +function playAgain(){ + location.reload(); +} \ No newline at end of file diff --git a/public/js/scores.js b/public/js/scores.js new file mode 100644 index 00000000..e4bed6be --- /dev/null +++ b/public/js/scores.js @@ -0,0 +1,65 @@ +addLoadEvent(() => { + const scores = $("#scoreModal"); + scores.on("show.bs.modal", () => fillScores(CURGAME)); +}); + +/** + * + */ +async function fillScores(game) { + if (!game.gameId) { // get game info off first game in dropdown + const dataSet = document.getElementById("scoreGameDD").firstElementChild.dataset; + game = {gameId: dataSet.gameId, name: dataSet.name, orderDir: dataSet.orderDir} + } + setScoresHeader(game.name); + const allScoresDataPromise = getScores(game.gameId, game.orderDir); + document.getElementById("globalScores").innerHTML = ""; + document.getElementById("userScores").innerHTML = ""; + document.getElementById("scoresLoading").classList.remove("d-none"); + const globalSource = document.getElementById("globalScoresBodyTemplate").innerHTML; + const globalTemplate = Handlebars.compile(globalSource); + const allScoresData = await allScoresDataPromise; + document.getElementById("scoresLoading").classList.add("d-none"); + document.getElementById("globalScores").innerHTML = globalTemplate({scoresData: allScoresData}); + let userScoresData; + if (auth.currentUser) { + userScoresData = allScoresData.filter(score => score.user.uid === auth.currentUser.uid); + } else { + userScoresData = []; + // TODO: Show waring about not being signed in + } + const userContext = {scoresData: userScoresData}; + const userSource = document.getElementById("userScoresBodyTemplate").innerHTML; + const userTemplate = Handlebars.compile(userSource); + document.getElementById("userScores").innerHTML = userTemplate(userContext); + +} + +async function getScores(gameId, orderDir) { + const url = `/data/games/${gameId}/scores`; + const request = new Request(url, { + method: "POST", + body: JSON.stringify({orderDir}), + headers: { + "Content-Type": "application/json", + }, + }); + console.log(request); + return fetch(request) + .then((resp) => resp.json()) + .then(function( arr ) { + // do something with the response + return arr; + }) + .catch(function (error) { + console.log( error ); + }); +} + +function onGameChange(elm) { + fillScores({gameId: elm.dataset.gameId, name: elm.dataset.name, orderDir: elm.dataset.orderDir}); +} + +function setScoresHeader(gameName) { + document.querySelector("#scoreModal .modal-header .modal-title").textContent = gameName + " Scores" +} \ No newline at end of file diff --git a/public/js/sudoku/sudoku.js b/public/js/sudoku/sudoku.js new file mode 100644 index 00000000..a5a10c0c --- /dev/null +++ b/public/js/sudoku/sudoku.js @@ -0,0 +1,161 @@ +let start1 = [0,0,0,2,6,0,7,0,1, + 6,8,0,0,7,0,0,9,0, + 1,9,0,0,0,4,5,0,0, + 8,2,0,1,0,0,0,4,0, + 0,0,4,6,0,2,9,0,0, + 0,5,0,0,0,3,0,2,8, + 0,0,9,3,0,0,0,7,4, + 0,4,0,0,5,0,0,3,6, + 7,0,3,0,1,8,0,0,0]; +let start2 = [ 1,0,0,4,8,9,0,0,6, + 7,3,0,0,0,0,0,4,0, + 0,0,0,0,0,1,2,9,5, + 0,0,7,1,2,0,6,0,0, + 5,0,0,7,0,3,0,0,8, + 0,0,6,0,9,5,7,0,0, + 9,1,4,6,0,0,0,0,0, + 0,2,0,0,0,0,0,3,7, + 8,0,0,5,1,2,0,0,4]; +let start3 = [ 0,2,0,6,0,8,0,0,0, + 5,8,0,0,0,9,7,0,0, + 0,0,0,0,4,0,0,0,0, + 3,7,0,0,0,0,5,0,0, + 6,0,0,0,0,0,0,0,4, + 0,0,8,0,0,0,0,1,3, + 0,0,0,0,2,0,0,0,0, + 0,0,9,8,0,0,0,3,6, + 0,0,0,3,0,6,0,9,0]; + +let possibleStarts = [start1,start2,start3]; + +let done1 = [4,3,5,2,6,9,7,8,1, + 6,8,2,5,7,1,4,9,3, + 1,9,7,8,3,4,5,6,2, + 8,2,6,1,9,5,3,4,7, + 3,7,4,6,8,2,9,1,5, + 9,5,1,7,4,3,6,2,8, + 5,1,9,3,2,6,8,7,4, + 2,4,8,9,5,7,1,3,6, + 7,6,3,4,1,8,2,5,9]; +let done2 = [1,5,2,4,8,9,3,7,6, + 7,3,9,2,5,6,8,4,1, + 4,6,8,3,7,1,2,9,5, + 3,8,7,1,2,4,6,5,9, + 5,9,1,7,6,3,4,2,8, + 2,4,6,8,9,5,7,1,3, + 9,1,4,6,3,7,5,8,2, + 6,2,5,9,4,8,1,3,7, + 8,7,3,5,1,2,9,6,4]; +let done3 = [1,2,3,6,7,8,9,4,5, + 5,8,4,2,3,9,7,6,1, + 9,6,7,1,4,5,3,2,8, + 3,7,2,4,6,1,5,8,9, + 6,9,1,5,8,3,2,7,4, + 4,5,8,7,9,2,6,1,3, + 8,3,6,9,2,4,1,5,7, + 2,1,9,8,5,7,4,3,6, + 7,4,5,3,1,6,8,9,2]; + +let possibleDones = [done1,done2,done3]; + +let currentPuzzle=0; + +let time = 0; +/** + * Starts a timer + */ +let seconds = setInterval(function(){ + time += 1; + + let minutes = Math.floor(time/60); + let seconds = Math.floor(time%60); + + if(seconds > 9){ + document.getElementById('timer').innerText = "Time " + minutes + ":" + seconds; + }else { + document.getElementById('timer').innerText = "Time " + minutes + ":0" + seconds; + } +},1000); + +/** + * loads the sudoku board from a random start position + */ +function load(){ + for(let i=1;i<=81;i++){ + let elem = document.querySelector('#cell-'+i+' input'); + elem.disabled = false; + elem.value = ''; + } + currentPuzzle = Math.floor((Math.random()*3)); + for(let i=1;i<=81;i++){ + let elem = document.querySelector('#cell-'+i+' input'); + elem.onchange = checkComplete; + if(possibleStarts[currentPuzzle][i-1]!==0){ + elem.disabled = true; + elem.value = possibleStarts[currentPuzzle][i-1]; + } + } +} + +load(); + +/** + * checks if the puzzle is solved + * + * @returns {boolean} true if finished, false if not + */ +function checkComplete(){ + for(let i=1;i<=81;i++){ + let elem = document.querySelector('#cell-'+i+' input'); + if(parseInt(elem.value) !== possibleDones[currentPuzzle][i-1]) { + console.log('not solved'); + return false; + } + } + console.log('solved'); + stopTimer(); + document.getElementById('win').style.display='block'; + document.getElementById('playAgain').style.display='block'; + + const scoreModel = $("#gameScoreFormModal"); + attachHeading(`Time: ${time} seconds`); + //scoreModel.on("hidden.bs.modal", load); + attachSubmit({score: time}, () => { + scoreModel.modal("hide"); + }); + scoreModel.modal("show"); // user jQuery to show the modal + + return true; +} + +/** + * stops the timer + */ +function stopTimer(){ + clearTimeout(seconds); +} + +/** + * makes sure the keyboard input + * is correct. Only numbers 1-9 + * + * @param {e} the keyboard event + */ +function validate(e){ + var theEvent = e || window.e; + // Handle key press + var key = theEvent.keyCode || theEvent.which; + key = String.fromCharCode(key); + var regex = /[1-9]|\./; + if( !regex.test(key) ) { + theEvent.returnValue = false; + if(theEvent.preventDefault) theEvent.preventDefault(); + } +} + +/** + * reloads the page + */ +function playAgain(){ + location.reload(); +} \ No newline at end of file diff --git a/public/js/tower-defence/astar.js b/public/js/tower-defence/astar.js new file mode 100644 index 00000000..9d385b0b --- /dev/null +++ b/public/js/tower-defence/astar.js @@ -0,0 +1,404 @@ +// javascript-astar 0.4.1 +// http://github.com/bgrins/javascript-astar +// Freely distributable under the MIT License. +// Implements the astar search algorithm in javascript using a Binary Heap. +// Includes Binary Heap (with modifications) from Marijn Haverbeke. +// http://eloquentjavascript.net/appendix2.html +(function(definition) { + /* global module, define */ + if (typeof module === 'object' && typeof module.exports === 'object') { + module.exports = definition(); + } else if (typeof define === 'function' && define.amd) { + define([], definition); + } else { + var exports = definition(); + window.astar = exports.astar; + window.Graph = exports.Graph; + } +})(function() { + + function pathTo(node) { + var curr = node; + var path = []; + while (curr.parent) { + path.unshift(curr); + curr = curr.parent; + } + return path; + } + + function getHeap() { + return new BinaryHeap(function(node) { + return node.f; + }); + } + + var astar = { + /** + * Perform an A* Search on a graph given a start and end node. + * @param {Graph} graph + * @param {GridNode} start + * @param {GridNode} end + * @param {Object} [options] + * @param {bool} [options.closest] Specifies whether to return the + path to the closest node if the target is unreachable. + * @param {Function} [options.heuristic] Heuristic function (see + * astar.heuristics). + */ + search: function(graph, start, end, options) { + graph.cleanDirty(); + options = options || {}; + var heuristic = options.heuristic || astar.heuristics.manhattan; + var closest = options.closest || false; + + var openHeap = getHeap(); + var closestNode = start; // set the start node to be the closest if required + + start.h = heuristic(start, end); + graph.markDirty(start); + + openHeap.push(start); + + while (openHeap.size() > 0) { + + // Grab the lowest f(x) to process next. Heap keeps this sorted for us. + var currentNode = openHeap.pop(); + + // End case -- result has been found, return the traced path. + if (currentNode === end) { + return pathTo(currentNode); + } + + // Normal case -- move currentNode from open to closed, process each of its neighbors. + currentNode.closed = true; + + // Find all neighbors for the current node. + var neighbors = graph.neighbors(currentNode); + + for (var i = 0, il = neighbors.length; i < il; ++i) { + var neighbor = neighbors[i]; + + if (neighbor.closed || neighbor.isWall()) { + // Not a valid node to process, skip to next neighbor. + continue; + } + + // The g score is the shortest distance from start to current node. + // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet. + var gScore = currentNode.g + neighbor.getCost(currentNode); + var beenVisited = neighbor.visited; + + if (!beenVisited || gScore < neighbor.g) { + + // Found an optimal (so far) path to this node. Take score for node to see how good it is. + neighbor.visited = true; + neighbor.parent = currentNode; + neighbor.h = neighbor.h || heuristic(neighbor, end); + neighbor.g = gScore; + neighbor.f = neighbor.g + neighbor.h; + graph.markDirty(neighbor); + if (closest) { + // If the neighbour is closer than the current closestNode or if it's equally close but has + // a cheaper path than the current closest node then it becomes the closest node + if (neighbor.h < closestNode.h || (neighbor.h === closestNode.h && neighbor.g < closestNode.g)) { + closestNode = neighbor; + } + } + + if (!beenVisited) { + // Pushing to heap will put it in proper place based on the 'f' value. + openHeap.push(neighbor); + } else { + // Already seen the node, but since it has been rescored we need to reorder it in the heap + openHeap.rescoreElement(neighbor); + } + } + } + } + + if (closest) { + return pathTo(closestNode); + } + + // No result was found - empty array signifies failure to find path. + return []; + }, + // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html + heuristics: { + manhattan: function(pos0, pos1) { + var d1 = Math.abs(pos1.x - pos0.x); + var d2 = Math.abs(pos1.y - pos0.y); + return d1 + d2; + }, + diagonal: function(pos0, pos1) { + var D = 1; + var D2 = Math.sqrt(2); + var d1 = Math.abs(pos1.x - pos0.x); + var d2 = Math.abs(pos1.y - pos0.y); + return (D * (d1 + d2)) + ((D2 - (2 * D)) * Math.min(d1, d2)); + } + }, + cleanNode: function(node) { + node.f = 0; + node.g = 0; + node.h = 0; + node.visited = false; + node.closed = false; + node.parent = null; + } + }; + + /** + * A graph memory structure + * @param {Array} gridIn 2D array of input weights + * @param {Object} [options] + * @param {bool} [options.diagonal] Specifies whether diagonal moves are allowed + */ + function Graph(gridIn, options) { + options = options || {}; + this.nodes = []; + this.diagonal = !!options.diagonal; + this.grid = []; + for (var x = 0; x < gridIn.length; x++) { + this.grid[x] = []; + + for (var y = 0, row = gridIn[x]; y < row.length; y++) { + var node = new GridNode(x, y, row[y]); + this.grid[x][y] = node; + this.nodes.push(node); + } + } + this.init(); + } + + Graph.prototype.init = function() { + this.dirtyNodes = []; + for (var i = 0; i < this.nodes.length; i++) { + astar.cleanNode(this.nodes[i]); + } + }; + + Graph.prototype.cleanDirty = function() { + for (var i = 0; i < this.dirtyNodes.length; i++) { + astar.cleanNode(this.dirtyNodes[i]); + } + this.dirtyNodes = []; + }; + + Graph.prototype.markDirty = function(node) { + this.dirtyNodes.push(node); + }; + + Graph.prototype.neighbors = function(node) { + var ret = []; + var x = node.x; + var y = node.y; + var grid = this.grid; + + // West + if (grid[x - 1] && grid[x - 1][y]) { + ret.push(grid[x - 1][y]); + } + + // East + if (grid[x + 1] && grid[x + 1][y]) { + ret.push(grid[x + 1][y]); + } + + // South + if (grid[x] && grid[x][y - 1]) { + ret.push(grid[x][y - 1]); + } + + // North + if (grid[x] && grid[x][y + 1]) { + ret.push(grid[x][y + 1]); + } + + if (this.diagonal) { + // Southwest + if (grid[x - 1] && grid[x - 1][y - 1]) { + ret.push(grid[x - 1][y - 1]); + } + + // Southeast + if (grid[x + 1] && grid[x + 1][y - 1]) { + ret.push(grid[x + 1][y - 1]); + } + + // Northwest + if (grid[x - 1] && grid[x - 1][y + 1]) { + ret.push(grid[x - 1][y + 1]); + } + + // Northeast + if (grid[x + 1] && grid[x + 1][y + 1]) { + ret.push(grid[x + 1][y + 1]); + } + } + + return ret; + }; + + Graph.prototype.toString = function() { + var graphString = []; + var nodes = this.grid; + for (var x = 0; x < nodes.length; x++) { + var rowDebug = []; + var row = nodes[x]; + for (var y = 0; y < row.length; y++) { + rowDebug.push(row[y].weight); + } + graphString.push(rowDebug.join(" ")); + } + return graphString.join("\n"); + }; + + function GridNode(x, y, weight) { + this.x = x; + this.y = y; + this.weight = weight; + } + + GridNode.prototype.toString = function() { + return "[" + this.x + " " + this.y + "]"; + }; + + GridNode.prototype.getCost = function(fromNeighbor) { + // Take diagonal weight into consideration. + if (fromNeighbor && fromNeighbor.x != this.x && fromNeighbor.y != this.y) { + return this.weight * 1.41421; + } + return this.weight; + }; + + GridNode.prototype.isWall = function() { + return this.weight === 0; + }; + + function BinaryHeap(scoreFunction) { + this.content = []; + this.scoreFunction = scoreFunction; + } + + BinaryHeap.prototype = { + push: function(element) { + // Add the new element to the end of the array. + this.content.push(element); + + // Allow it to sink down. + this.sinkDown(this.content.length - 1); + }, + pop: function() { + // Store the first element so we can return it later. + var result = this.content[0]; + // Get the element at the end of the array. + var end = this.content.pop(); + // If there are any elements left, put the end element at the + // start, and let it bubble up. + if (this.content.length > 0) { + this.content[0] = end; + this.bubbleUp(0); + } + return result; + }, + remove: function(node) { + var i = this.content.indexOf(node); + + // When it is found, the process seen in 'pop' is repeated + // to fill up the hole. + var end = this.content.pop(); + + if (i !== this.content.length - 1) { + this.content[i] = end; + + if (this.scoreFunction(end) < this.scoreFunction(node)) { + this.sinkDown(i); + } else { + this.bubbleUp(i); + } + } + }, + size: function() { + return this.content.length; + }, + rescoreElement: function(node) { + this.sinkDown(this.content.indexOf(node)); + }, + sinkDown: function(n) { + // Fetch the element that has to be sunk. + var element = this.content[n]; + + // When at 0, an element can not sink any further. + while (n > 0) { + + // Compute the parent element's index, and fetch it. + var parentN = ((n + 1) >> 1) - 1; + var parent = this.content[parentN]; + // Swap the elements if the parent is greater. + if (this.scoreFunction(element) < this.scoreFunction(parent)) { + this.content[parentN] = element; + this.content[n] = parent; + // Update 'n' to continue at the new position. + n = parentN; + } + // Found a parent that is less, no need to sink any further. + else { + break; + } + } + }, + bubbleUp: function(n) { + // Look up the target element and its score. + var length = this.content.length; + var element = this.content[n]; + var elemScore = this.scoreFunction(element); + + while (true) { + // Compute the indices of the child elements. + var child2N = (n + 1) << 1; + var child1N = child2N - 1; + // This is used to store the new position of the element, if any. + var swap = null; + var child1Score; + // If the first child exists (is inside the array)... + if (child1N < length) { + // Look it up and compute its score. + var child1 = this.content[child1N]; + child1Score = this.scoreFunction(child1); + + // If the score is less than our element's, we need to swap. + if (child1Score < elemScore) { + swap = child1N; + } + } + + // Do the same checks for the other child. + if (child2N < length) { + var child2 = this.content[child2N]; + var child2Score = this.scoreFunction(child2); + if (child2Score < (swap === null ? elemScore : child1Score)) { + swap = child2N; + } + } + + // If the element needs to be moved, swap it, and continue. + if (swap !== null) { + this.content[n] = this.content[swap]; + this.content[swap] = element; + n = swap; + } + // Otherwise, we are done. + else { + break; + } + } + } + }; + + return { + astar: astar, + Graph: Graph + }; + +}); \ No newline at end of file diff --git a/public/js/tower-defence/defs.js b/public/js/tower-defence/defs.js new file mode 100644 index 00000000..fcc698d7 --- /dev/null +++ b/public/js/tower-defence/defs.js @@ -0,0 +1,125 @@ +// Declare myGame, the object that contains our game's states +const myGame = { + width: 800, + height: 600, + //Define our game states + scenes: [], + + // Define common framerate to be referenced in animations + frameRate: 20 +}; + +const cellTypes = { + WALL: -2, // this tile cannot be passed, have anything on it or be shot though + BASE: -1, + OPEN: 0, + TOWER: 1, // a placed tower + MENU_SAND: 2, + MENU_MG: 3, + MENU_CANNON: 4, +}; + + +let waveSpacingDur = 40; // how many update cycles to wait until moving +let wavePauseTime = 200; +let difInc = 0.08; +const offScreenCoord = {x: -500, y: -500}; + +const cellSize = {width: 40, height: 40}; +const playArea = {width: (myGame.width / cellSize.width), height: (myGame.height / cellSize.height) - 1}; +const menuArea = {width: (myGame.width / cellSize.width), height: 1}; +const enemyEnterCoord = {x: playArea.width, y: 1}; // is off playArea but grid has an empty cell here +const btmLeft = {x: 0, y: playArea.height + menuArea.height - menuArea.height - 1}; // the bottom left cell of the grid. where the base is +const baseEntrance = {x: btmLeft.x, y: btmLeft.y}; + +const grid = []; +function resetGrid() { + for (let i = 0; i < playArea.height; i++) { + grid[i] = []; + for (let j = 0; j < playArea.width; j++) { + if (i === 0 || j === 0 || i === playArea.height - 1 || (i !== enemyEnterCoord.y && j === playArea.width - 1)) { + // add WALLs to top, bottom, left, and right. Except the second one down on the right + grid[i][j] = cellTypes.WALL; + } else { + grid[i][j] = cellTypes.OPEN; + } + } + } + for (let i = playArea.height; i < playArea.height + menuArea.height; i++) { + grid[i] = []; + for (let j = 0; j < menuArea.width; j++) { + if (j === 6) { + grid[i][j] = cellTypes.MENU_SAND; + } else if (j === 7) { + grid[i][j] = cellTypes.MENU_MG; + } else if (j === 8) { + grid[i][j] = cellTypes.MENU_CANNON; + } else { + grid[i][j] = cellTypes.WALL; + } + } + } + // game and cell size must allow base to fit in 2x2 cells in bottom left corner + // overrides some WALLs + grid[btmLeft.y][btmLeft.x] = cellTypes.BASE; + grid[btmLeft.y][btmLeft.x + 1] = cellTypes.BASE; + grid[btmLeft.y - 1][btmLeft.x] = cellTypes.BASE; + grid[btmLeft.y - 1][btmLeft.x + 1] = cellTypes.BASE; + + // add enemy buffer area + grid[enemyEnterCoord.y][enemyEnterCoord.x] = cellTypes.OPEN; + +} +resetGrid(); +console.log(grid); + +/** + * return a weighted graph to be used in astar + * @author: jk + * @returns {[]} + */ +function getGraph() { + const graph = []; + for (let i = 0; i < grid.length; i++) { // height + graph[i] = []; + for (let j = 0; j < grid[i].length; j++) { // width + switch (grid[i][j]) { + case cellTypes.WALL: + case cellTypes.TOWER: + default: // all tower types + graph[i][j] = 0; // impassible (infinity wight) + break; + case cellTypes.OPEN: + case cellTypes.BASE: + graph[i][j] = 1; + break; + } + } + } + return graph; +} + +function getPath(startCoord) { + const graphData = getGraph(); + // console.log("graph data:", graphData, startCoord); + const graph = new Graph(graphData, {diagonal: true}); + // x and y are switched in grid + const start = graph.grid[startCoord.y][startCoord.x]; + const end = graph.grid[baseEntrance.y][baseEntrance.x]; + const path = astar.search(graph, start, end, {heuristic: astar.heuristics.diagonal}); + // console.log("path:", path); + return path; +} + +function getPathPoints(startCoord) { + const pathPoints = {x: [], y: []}; + // start off the screen and first go to the starting point + pathPoints.x.push(startCoord.x); + pathPoints.y.push(startCoord.y); + getPath(startCoord).forEach(node => { + // x and y are switched in grid + pathPoints.x.push(node.y); + pathPoints.y.push(node.x); + }); + return pathPoints +} \ No newline at end of file diff --git a/public/js/tower-defence/game-play.js b/public/js/tower-defence/game-play.js new file mode 100644 index 00000000..97363383 --- /dev/null +++ b/public/js/tower-defence/game-play.js @@ -0,0 +1,600 @@ +const gamePlayState = new Phaser.Class({ + // Define scene + Extends: Phaser.Scene, + initialize: + function GamePlay(){ + Phaser.Scene.call(this, {key: "GamePlay"}); + }, + + preload: function() { + // Preload images for this state + }, + + create: function() { + const scene = this; + console.log(scene); + scene.menuSelection = null; // set to {type, add} when has a value of types cellTypes, function + this.pause = false; + + this.score = 0; + this.money = 400; + this.lives = 10; + + // Create objects + console.log("GamePlay"); + // const pistol = scene.add.image(250, 300, "pistol"); + const station = scene.add.image((btmLeft.x + 1) * cellSize.width, (btmLeft.y) * cellSize.height, "station"); + initFromGrid(scene); // add bricks + + scene.enemies = []; + scene.towers = []; + // Enemy types + function removeEnemys(enemy) { + scene.enemies.forEach((enemy, idx) => { + if (!enemy.isAlive()) { + scene.enemies.splice(idx, 1); + } + }); + console.log(scene.enemies); + } + const enemy_events = {onBase: () => { + console.log('-1 life'); + removeEnemys(); + return scene; + }, onDeath: () => { + console.log('-1 Enemy'); + removeEnemys(); + return scene; + }}; + scene.EnemyUnits = {Trucks: Truck(enemy_events, scene)}; + scene.Towers = {SandBags: SandBag(), MachineGuns: MachineGun(), Cannons: Cannon()}; + + scene.addTruck3b = function (waveIndex) { + scene.enemies.push(scene.EnemyUnits.Trucks.create(scene.EnemyUnits.Trucks.Truck3b(scene), waveIndex)); + }; + + scene.addSandBag = function (coord) { + scene.towers.push(scene.Towers.SandBags.create(scene.Towers.SandBags.Sand(scene, coord))); + }; + scene.addMachineGun = function (coord) { + scene.towers.push(scene.Towers.MachineGuns.create(scene.Towers.MachineGuns.MachineGun(scene, coord))); + }; + scene.addCannon = function (coord) { + scene.towers.push(scene.Towers.Cannons.create(scene.Towers.Cannons.Cannon(scene, coord))); + }; + + // scene.addTruck3b(1); + // scene.addSandBag({x: 1, y: 1}); + + // scene.enemies.push(scene.EnemyUnits.Trucks.create(scene.EnemyUnits.Trucks.Truck3b(scene), 2)); + // scene.enemies.push(scene.EnemyUnits.Trucks.create(scene.EnemyUnits.Trucks.Truck3b(scene), 3)); + // scene.enemies.push(scene.EnemyUnits.Trucks.create(scene.EnemyUnits.Trucks.Truck3b(scene), 4)); + + // cursor + this.cursor = this.add.graphics(); + this.cursor.lineStyle(2, 0x000000, 1); + this.cursor.strokeRect(0, 0, 40, 40); + + // wave values + this.waveIdx = 0; + this.waveAmount = 3; + this.spacingChoices = [40, 30, 20, 15]; + + // ui text + this.waveText = scene.add.text(0, 0, `Wave: ${this.waveIdx}`, {fontSize: 22, color: "#000000", backgroundColor: "#ffffff"}); + this.livesText = scene.add.text(0, 30, `Lives: ${this.lives}`, {fontSize: 22, color: "#000000", backgroundColor: "#ffffff"}); + this.scoreText = scene.add.text(160, 0, `Score: ${this.score}`, {fontSize: 22, color: "#000000", backgroundColor: "#ffffff"}); + this.moneyText = scene.add.text(160, 30, `Money: $${this.money}`, {fontSize: 22, color: "#000000", backgroundColor: "#ffffff"}); + + const uiContainer = scene.add.container((playArea.width - 9) * cellSize.width, (playArea.height - 1) * cellSize.height); + uiContainer.add(this.scoreText); + uiContainer.add(this.moneyText); + uiContainer.add(this.waveText); + uiContainer.add(this.livesText); + + this.sandText = scene.add.text(0, 0, `Sandbag\n$${scene.Towers.SandBags.price}`, {fontSize: 12, color: "#ffffff", backgroundColor: "#000000"}); + this.mgText = scene.add.text(cellSize.width , 0, `Machine Gun\n$${scene.Towers.MachineGuns.price}`, {fontSize: 12, color: "#ffffff", backgroundColor: "#000000"}); + this.cannonText = scene.add.text(cellSize.width * 2, 0, `Cannon\n$${scene.Towers.Cannons.price}`, {fontSize: 12, color: "#ffffff", backgroundColor: "#000000"}); + + const towerUiContainer = scene.add.container(6 * cellSize.width, (playArea.height - 1) * cellSize.height); + towerUiContainer.add(this.sandText); + towerUiContainer.add(this.mgText); + towerUiContainer.add(this.cannonText); + + scene.pauseKey = scene.input.keyboard.addKey("P"); + scene.prevPauseDown = false; + scene.gameover = false; + + }, + + update: function() { + const scene = this; + if (scene.pauseKey.isDown) { + scene.prevPauseDown = true; + } + if (scene.pauseKey.isUp && scene.prevPauseDown) { + scene.pause = !scene.pause; + scene.prevPauseDown = false; + } + if (scene.pause || scene.gameover) return; + if (scene.lives <= 0) { + scene.gameover = true; + const scoreModel = $("#gameScoreFormModal"); + attachHeading(`Score: ${scene.score.toFixed(0)}`); + scoreModel.on("hidden.bs.modal", () => { + scene.scene.start(); + resetGrid(); + }); + attachSubmit({score: scene.score}, () => { + scoreModel.modal("hide"); + }); + scoreModel.modal("show"); // user jQuery to show the modal + } + // console.log(scene.enemies); + if (scene.enemies.length === 0) { + if (this.waveIdx > 10) { + waveSpacingDur = this.spacingChoices[Math.floor(Math.random() * this.spacingChoices.length + (this.waveIdx / 5)) % this.spacingChoices.length]; + } + this.waveIdx++; + if (this.waveIdx % 2 === 0) { + this.waveAmount += 1; + } + for (let i = 0; i < this.waveAmount; i++) { + scene.addTruck3b(i); + } + } + updateUiText(scene); + updateCursor(scene); + scene.enemies.forEach(enemy => { + enemy.move(); + }); + scene.towers.forEach(tower => { + tower.resetTargets(scene.enemies); + tower.shoot(); + }); + + }, + + + +}); + +// Create + +function initFromGrid(scene) { + for (let i = 0; i < grid.length; i++) { // height + for (let j = 0; j < grid[i].length; j++) { // width + switch (grid[i][j]) { + case cellTypes.WALL: + scene.add.image(j * cellSize.width, i * cellSize.height, "brick1").setOrigin(0, 0); + break; + case cellTypes.MENU_SAND: + scene.add.image(j * cellSize.width, i * cellSize.height, "sand").setOrigin(0, 0); + break; + case cellTypes.MENU_MG: + scene.add.image(j * cellSize.width, i * cellSize.height, "machine gun").setOrigin(0, 0); + break; + case cellTypes.MENU_CANNON: + scene.add.image(j * cellSize.width, i * cellSize.height, "cannon").setOrigin(0, 0); + break; + + } + } + } +} + + +// Update + +function updateUiText(scene) { + scene.waveText.setText(`Wave: ${scene.waveIdx}`); + scene.livesText.setText(`Lives: ${scene.lives}`); + scene.scoreText.setText(`Score: ${scene.score}`); + scene.moneyText.setText(`Money: $${scene.money}`); +} + + +function placeTower(scene, coord) { + console.log(scene); + if (scene.menuSelection && grid[coord.y][coord.x] === cellTypes.OPEN && scene.menuSelection.cost <= scene.money) { + grid[coord.y][coord.x] = cellTypes.TOWER; + if (getPath(enemyEnterCoord).length === 0) { + grid[coord.y][coord.x] = cellTypes.OPEN; // reset to open since this blocks a path from the entrance + } else if (scene.menuSelection) { + scene.money -= scene.menuSelection.cost; + scene.menuSelection.add(coord); + } + } +} + +function clearTowerSelection(scene) { + scene.menuSelection = null; +} + +function selectTower(scene, coord) { + switch (grid[coord.y][coord.x]) { + case cellTypes.MENU_SAND: + scene.menuSelection = {type: cellTypes.MENU_SAND, cost: scene.Towers.SandBags.price, add: scene.addSandBag}; + break; + case cellTypes.MENU_MG: + scene.menuSelection = {type: cellTypes.MENU_MG, cost: scene.Towers.MachineGuns.price, add: scene.addMachineGun}; + break; + case cellTypes.MENU_CANNON: + scene.menuSelection = {type: cellTypes.MENU_CANNON, cost: scene.Towers.Cannons.price, add: scene.addCannon}; + break; + default: + clearTowerSelection(scene); + break; + } + +} + +function updateCursor(scene) { + const coord = {x: Math.floor(scene.input.activePointer.worldX / cellSize.width), y: Math.floor(scene.input.activePointer.worldY / cellSize.height)}; + scene.cursor.x = coord.x * cellSize.width; + scene.cursor.y = coord.y * cellSize.height; + + if (scene.input.mousePointer.isDown) { + if (coord.y < playArea.height ) { + placeTower(scene, coord); + } else if (coord.y >= playArea.height && coord.y < playArea.height + menuArea.height) { + selectTower(scene, coord); + } + } +} + + + + + + +/** + * A closure creating a Enemy. + * @param sprite The sprite for this enemy. The enemy will move this sprite. + * @param waveIndex The position in the wave used to space the enemies out. + * @param moveSpeed This enemies move speed. + * @param healthPoints This enemies starting health points. + * @param onDeath A call back that will be called on the death of this Enemy. + * @param onBase + * @param scene + * @returns {{damage: function(number), move: function, sprite: *}} + * @constructor + */ +function Enemy(sprite, waveIndex, moveSpeed, healthPoints, onDeath, onBase, scene) { + let pathPoints; + let moveIdx = 0; + let atBase = false; + const spriteRotation = sprite.rotation; // should be passed in facing right + let hp = healthPoints; + let alive = true; + let waveWaitDur = waveSpacingDur * waveIndex + wavePauseTime; + let healthBar; + let healthBarBg; + + function move() { + if (atBase || !alive) return false; // halt movement + if (waveWaitDur >= 0) { + waveWaitDur--; + if (waveWaitDur < 0) { + // set the sprite to the correct position + createHeathBar(); + sprite.x = enemyEnterCoord.x * cellSize.width; + sprite.y = enemyEnterCoord.y * cellSize.height; + } else { + return true; + } + } + const coord = getSpriteCoord(); // coords at the beginning of this update call (after setting init pos above) + let newCoordTrans; + let newPathCoord; + if (!pathPoints) { + pathPoints = getPathPoints(coord); + } + if (pathPoints) { + // CatmullRom + newCoordTrans = {x: Phaser.Math.Interpolation.CatmullRom(pathPoints.x, moveIdx), y: Phaser.Math.Interpolation.CatmullRom(pathPoints.y, moveIdx)}; + // x and y paths can have different lengths + const pathIdxX = (moveIdx < 1) ? Math.floor(pathPoints.x.length * moveIdx) : pathPoints.x.length - 1; + const pathIdxY = (moveIdx < 1) ? Math.floor(pathPoints.y.length * moveIdx) : pathPoints.y.length - 1; + newPathCoord = {x: pathPoints.x[pathIdxX], y: pathPoints.y[pathIdxY]}; + // TODO: Recalculate path if now is occupied + // if (grid[newPathCoord.y][newPathCoord.x] !== cellTypes.OPEN || grid[newPathCoord.y][newPathCoord.x] !== cellTypes.BASE) { // if moving into a new coord and it is occupied + // pathPoints = getPathPoints(); + // } + } + if (!atBase) { + const newPos = {x: newCoordTrans.x * cellSize.width + (cellSize.width / 2), y: newCoordTrans.y * cellSize.height + (cellSize.width / 2)}; + const angle = Phaser.Math.Angle.Between(sprite.x, sprite.y, newPos.x, newPos.y); + sprite.rotation = spriteRotation + angle; + sprite.x = newPos.x; + sprite.y = newPos.y; + updateHealthBarPos(); + moveIdx += (moveSpeed * 2 / (pathPoints.x.length + pathPoints.y.length) ); + checkAtBase(newPathCoord); + } + } + + function getSpriteCoord() { + return {x: Math.floor(sprite.x / cellSize.width), y: Math.floor(sprite.y / cellSize.height)} + } + + /** + * Check if at the base + * @param coord Coordinates from path + */ + function checkAtBase(coord) { + if (coord.x <= baseEntrance.x && coord.y >= baseEntrance.y) { + atBase = true; + alive = false; + die(onBase()); + } + } + + function createHeathBar() { + healthBarBg = scene.add.graphics(); + healthBarBg.fillStyle(0x000000, 1); + healthBarBg.fillRect(0, 0, cellSize.width, 6); + healthBar = scene.add.graphics(); + healthBar.fillStyle(0x00ff00, 1); + healthBar.fillRect(0, 0, cellSize.width, 6); + } + + function updateHealthBarPos() { + healthBar.x = sprite.x - (cellSize.width / 2); + healthBar.y = sprite.y - (cellSize.height / 2); + healthBarBg.x = sprite.x - (cellSize.width / 2); + healthBarBg.y = sprite.y - (cellSize.height / 2); + } + + function updateHealthBarHealth() { + if (!healthBar) return; + healthBar.scaleX = (hp > 0) ? (hp / healthPoints) : 0; + if (healthBar.scaleX < .6 && healthBar.scaleX > .3) { + healthBar.fillStyle(0xeeee00, 1); + healthBar.fillRect(0, 0, cellSize.width, 6); + } else if (healthBar.scaleX < .3) { + healthBar.fillStyle(0xff0000, 1); + healthBar.fillRect(0, 0, cellSize.width, 6); + } + } + + /** + * Reduce this enemies health + * @param damageAmount The number of hp to deduct + * @returns {boolean|undefined} True if damage taken; false if already dead; undefined if damaged and is now dead + */ + function damage(damageAmount) { + if (!alive) return false; // return false if the enemy is already dead + hp -= damageAmount; // damage the enemy + updateHealthBarHealth(); + if (isDead()) { + alive = false; + die(onDeath()); + return undefined; // return undefined if the enemy is now dead + } + return true; // return true if damage taken + } + + function die(scene) { + if (atBase) { + scene.lives -= 1; + } else { + scene.score += getScoreValue(); + scene.money += getMoneyValue(); + } + let fadeTween = scene.tweens.add({ + targets: [sprite, healthBar, healthBarBg], + alpha: { from: 1, to: 0 }, + ease: 'Linear', // 'Cubic', 'Elastic', 'Bounce', 'Back' + duration: 1000, + repeat: 0, // -1: infinity + yoyo: false, + onComplete: () => { + sprite.destroy(); + healthBar.destroy(); + healthBarBg.destroy(); + } + }); + } + + function isDead() { + return (hp < 0); + } + + function isAlive() { + return alive; + } + + function getScoreValue() { + return 4; + } + + function getMoneyValue() { + return Math.round(healthPoints * moveSpeed * 4 * (1 + difInc)); + } + + return {sprite, move, damage, isAlive}; +} + +// Update +function Tower(sprite, range, damage, fireRate) { + let targets = []; // list of all enemies in range + let target; // the current enemy being targeted + let shootIdx = 0; + const spriteRotation = sprite.rotation; // should be passed in facing right + + + /** + * Goes through all the given enemies and stores them if in range + * @param enemies All of the Enemy objects + */ + function resetTargets(enemies) { + targets = enemies.filter(isInRange); + } + + /** + * Determine if the given enemy is within this towers range + * @param enemy + * @returns {boolean} True when in range, else false + */ + function isInRange(enemy) { + return Phaser.Math.Distance.Between(sprite.x / cellSize.width, sprite.y / cellSize.height, enemy.sprite.x / cellSize.width, enemy.sprite.y / cellSize.height) < range; + } + + /** + * Confirm that the current target is in range and non-null + * @author: jk + * @returns {boolean} True when confirmed, else false. + */ + function confirmTarget() { + if (!(target && target.isAlive() && isInRange(target))) { // if no current target get the next one in range + for (let i = 0; i < targets.length; i++) { + if (isInRange(targets[i]) && targets[i].isAlive()) { + target = targets[i]; + targets.splice(0, i); // remove all enemies before the one found + break; + } + } + } + return target && target.isAlive() && isInRange(target); + } + + /** + * Inflict damage on the target. Handels cases where the target is already dead or is null by finding a new one. + * @returns {boolean} + */ + function shoot() { + if (shootIdx === 0 && confirmTarget()) { + faceTarget(); + switch(target.damage(damage)) { + case false: + console.log("bad target"); + shootIdx = 0; + return false; // bad target so try again next update + case true: + shootIdx = fireRate; + return true; + case undefined: + targets.splice(targets.indexOf(target), 1); // remove the enemy from the targets + shootIdx = 0; + return true; + } + } else { + if (shootIdx > 0) { + shootIdx--; + } else if (shootIdx < 0) { + shootIdx = fireRate; + } + } + return false; // no targets + } + + function faceTarget() { + const angle = Phaser.Math.Angle.Between(sprite.x, sprite.y, target.sprite.x, target.sprite.y); + sprite.rotation = spriteRotation + angle; + } + + return {sprite, shoot, resetTargets}; +} + + +// ENEMIES // + + +function Truck(events, scene) { + /** + * Create the enemy. + * @param sprite The sprite the enemy will use. + * @param waveIndex The position in the wave used to space the enemies out. + * @returns {{damage: (function(number)), move: Function, sprite: *}} + */ + function create(sprite, waveIndex) { + return Enemy(sprite, waveIndex, 0.025 + (0.002 * scene.waveIdx * (difInc + 1)), 55 + (25 * scene.waveIdx * (difInc + 1) * (scene.towers.length / 3)), events.onDeath, events.onBase, scene); + } + + function Truck3b(scene) { + const truck = scene.add.image(offScreenCoord.x, offScreenCoord.y, "truck3b"); + truck.angle = 90; + return truck; + } + + return {create, Truck3b}; +} + + +// TOWERS // + +function SandBag() { + const price = 30; + /** + * Create the Tower at the coordinates. + * @returns {{resetTargets: *, sprite: *, shoot: *}} + * @param {Phaser.GameObjects.Image} sprite The sprite to use + */ + function create(sprite) { + return Tower(sprite, 0, 0, 0); + } + + /** + * Create the Tower at the coordinates. + * @param scene The game object. + * @param coord The coordinates of the cell to place at. + * @returns {Phaser.GameObjects.Image} The sprite. + */ + function Sand(scene, coord) { + return scene.add.image(coord.x * cellSize.width + (cellSize.width / 2), coord.y * cellSize.height + (cellSize.height / 2), "sand").setOrigin(0.5, 0.5); + } + + return {create, price, Sand} +} + +function MachineGun() { + const price = 160; + /** + * Create the Tower at the coordinates. + * @returns {{resetTargets: *, sprite: *, shoot: *}} + * @param {Phaser.GameObjects.Image} sprite The sprite to use + */ + function create(sprite) { + return Tower(sprite, 2, 5, 3); + } + + /** + * Create the Tower at the coordinates. + * @param scene The game object. + * @param coord The coordinates of the cell to place at. + * @returns {Phaser.GameObjects.Image} The sprite. + */ + function MachineGun(scene, coord) { + return scene.add.image(coord.x * cellSize.width + (cellSize.width / 2), coord.y * cellSize.height + (cellSize.height / 2), "machine gun").setOrigin(0.5, 0.5).setRotation(Math.PI / 2); + } + + return {create, price, MachineGun} +} + +function Cannon() { + const price = 280; + /** + * Create the Tower at the coordinates. + * @returns {{resetTargets: *, sprite: *, shoot: *}} + * @param {Phaser.GameObjects.Image} sprite The sprite to use + */ + function create(sprite) { + return Tower(sprite, 3, 20, 15); + } + + /** + * Create the Tower at the coordinates. + * @param scene The game object. + * @param coord The coordinates of the cell to place at. + * @returns {Phaser.GameObjects.Image} The sprite. + */ + function Cannon(scene, coord) { + return scene.add.image(coord.x * cellSize.width + (cellSize.width / 2), coord.y * cellSize.height + (cellSize.height / 2), "cannon").setOrigin(0.5, 0.5).setRotation(Math.PI / 2); + } + + return {create, price, Cannon} +} + +// Add scene to list of scenes +myGame.scenes.push(gamePlayState); \ No newline at end of file diff --git a/public/js/tower-defence/main-menu.js b/public/js/tower-defence/main-menu.js new file mode 100644 index 00000000..e35d91ca --- /dev/null +++ b/public/js/tower-defence/main-menu.js @@ -0,0 +1,24 @@ +const mainMenuState = new Phaser.Class({ + // Define scene + Extends: Phaser.Scene, + initialize: + function MainMenu(){ + Phaser.Scene.call(this, {key: "MainMenu"}); + }, + + preload: function() { + // Preload images for this state + }, + + create: function() { + console.log("MainMenu"); + game.scene.start("GamePlay"); + }, + + update: function() { + // Update objects & variables + } +}); + +// Add scene to list of scenes +myGame.scenes.push(mainMenuState); \ No newline at end of file diff --git a/public/js/tower-defence/preload.js b/public/js/tower-defence/preload.js new file mode 100644 index 00000000..86705f31 --- /dev/null +++ b/public/js/tower-defence/preload.js @@ -0,0 +1,29 @@ +const preloadState = new Phaser.Class({ + Extends: Phaser.Scene, + initialize: + function Preload(){ + Phaser.Scene.call(this, {key: "Preload"}); + }, + preload: function() { + // Preload images for this state + this.load.svg("brick1", "/assets/tower-defence/brick1.svg", cellSize); + this.load.svg("pistol", "/assets/tower-defence/pistol-gun.svg", cellSize); + this.load.svg("station", "/assets/tower-defence/station.svg", {width: 2 * cellSize.width, height: 2 * cellSize.height}); + this.load.svg("machine gun", "/assets/tower-defence/machine-gun.svg", cellSize); + this.load.svg("cannon", "/assets/tower-defence/cannon.svg", cellSize); + this.load.svg("sand", "/assets/tower-defence/sand.svg", cellSize); + this.load.image("truck3b", "/assets/tower-defence/truck3b_body.png"); + }, + + create: function() { + console.log("Preload"); + game.scene.start("MainMenu"); + }, + + update: function() { + // Update objects & variables + } +}); + +// Add scene to list of scenes +myGame.scenes.push(preloadState); \ No newline at end of file diff --git a/routes/auth-routes.js b/routes/auth-routes.js new file mode 100644 index 00000000..b5553ddb --- /dev/null +++ b/routes/auth-routes.js @@ -0,0 +1,66 @@ +const express = require("express"); +const authRouter = express.Router(); + +const firebaseAdmin = require("firebase-admin"); +const db = firebaseAdmin.firestore(); +const auth = firebaseAdmin.auth(); + +/** + * Create the email and password account in firebase auth. + * Create a user object in the database with the same id as the auth user object. + * Return the custom token to the front end. + * @author: jk + */ +authRouter.post("/sign-up", async (req, res) => { + const data = req.body; + console.log(data); + auth.createUser({ + email: data.email, + emailVerified: false, + // phoneNumber: "+11234567890", + password: data.password, + displayName: data.name, + // photoURL: "http://www.example.com/12345678/photo.png", + disabled: false + }) + .then(async function(userRecord) { + // See the UserRecord reference doc for the contents of userRecord. + console.log("Successfully created new user:", userRecord.uid); + + // add user to collections + // dont need to wait + // db.collection("users").doc(userRecord.uid).set({ + // // no extra data needed so far + // }); + + await res.json({ + customToken: await getCustomToken(userRecord), + }); + }) + .catch(function(error) { + console.log("Error creating new user:", error); + res.json({ + failMsg: error.message, + }); + }); + +}); + + +/** + * Get a token for the user so the user can sign in. + * @author: jk + * @param {admin.auth.UserRecord} user The user database object. + * @returns {Promise} The token to be used on the front end to sign in. + */ +async function getCustomToken(user) { + return await firebaseAdmin.auth().createCustomToken(user.uid) + .then(function(customToken) { + return customToken; + }) + .catch(function(error) { + console.log("Error creating custom token:", error); + }); +} + +module.exports = authRouter; diff --git a/routes/cs-clicker-routes.js b/routes/cs-clicker-routes.js new file mode 100644 index 00000000..e69de29b diff --git a/routes/game-data-routes.js b/routes/game-data-routes.js new file mode 100644 index 00000000..39980268 --- /dev/null +++ b/routes/game-data-routes.js @@ -0,0 +1,36 @@ +const pull = require("./src/pullData"); +const push = require("./src/pushData"); + +const express = require("express"); +const gameDataRouter = express.Router(); + +/** + * Take in data from a game and store it in the database. + * @author: jk + */ +gameDataRouter.post("/:gameId", function(req, res) { + const data = req.body; + console.log("Game data submitted"); + console.log(req.params.gameId); + console.log(data); + if (data.userId) { + push.addScoreDefault(req.params.gameId, data.score, data.userId); + res.json({}); + } else { + res.json({error: "missing user"}); + } +}); + +/** + * Send back json of all scores. + * @author: jk + */ +gameDataRouter.post("/:gameId/scores", async function(req, res) { + const data = req.body; + console.log("Getting score data"); + console.log(req.params.gameId); + console.log(data); + res.json(await pull.getGameScores(req.params.gameId, data.orderDir)); +}); + +module.exports = gameDataRouter; diff --git a/routes/game-routes.js b/routes/game-routes.js new file mode 100644 index 00000000..14bc6a67 --- /dev/null +++ b/routes/game-routes.js @@ -0,0 +1,22 @@ +const pull = require("./src/pullData"); +const express = require("express"); +const gameRouter = express.Router(); + +/** + * Load the game object from the database to get it's path. Then render the game page + * @author: jk + */ +gameRouter.get("/:gameId", async function(req, res) { + const gameData = await pull.getGames(); + const dropdownData = pull.getGameDropdownData(gameData); + const game = gameData.find(game => game.gameId === req.params.gameId); + console.log(gameData); + if (game) { + res.render(game.path, {gameId: game.gameId, gameName: game.name, gameOrderDir: game.scoreOrder, scoresGameDropdownData: dropdownData}); + } else { + res.render("not-found", {msg: "We apologise. This game does not exist."}); + } +}); + + +module.exports = gameRouter; diff --git a/routes/index-routes.js b/routes/index-routes.js new file mode 100644 index 00000000..3131dc0b --- /dev/null +++ b/routes/index-routes.js @@ -0,0 +1,23 @@ +const pull = require("./src/pullData"); +const express = require("express"); +const indexRouter = express.Router(); + +indexRouter.get("/", async function(req, res) { + const gameData = await pull.getGames(); + const dropdownData = pull.getGameDropdownData(gameData); + res.render("index", {gameData: gameData, scoresGameDropdownData: dropdownData}); +}); + +indexRouter.get('/users/:uid', async function(req, res) { + const userData = await pull.getUserData(req.params.uid); + const context = { + userData: [ + 'Name: ' + userData.name, + 'Email: ' + userData.email, + 'Member Since: ' + userData.memberSince, + ] + }; + res.render('user', context); +}); + +module.exports = indexRouter; \ No newline at end of file diff --git a/routes/src/pullData.js b/routes/src/pullData.js new file mode 100644 index 00000000..ba171008 --- /dev/null +++ b/routes/src/pullData.js @@ -0,0 +1,144 @@ +const firebaseAdmin = require("firebase-admin"); +const db = firebaseAdmin.firestore(); +const auth = firebaseAdmin.auth(); + +/** + * Get the all game data from database. Not including scores. + * @author: jk + * @returns Array[{Promise<{gameId: *, path: *, playCount: *, name: *}>}] + */ +async function getGames() { + const gamesQuery = db.collection("games").orderBy("timesPlayed", "desc"); + return await Promise.all(await gamesQuery.get() + .then(snapshot => { + return snapshot.docs.map(function(doc) { + const data = doc.data(); + if (!data) + return; // bad game; return nothing + return { + name: data.name, + shortDesc: data.shortDesc, + path: data.path, + imgPath: data.imgPath, + playCount: data.timesPlayed, + gameId: doc.id, + scoreOrder: data.scoreOrder, + }; + + }); + }) + .catch(err => { + console.log("Error getting Game documents", err); + })); +} + +/** + * Get the all game data from database. Not including scores. + * @author: jk + * @param {string} gameId The id of the game in the database. + * @returns {Promise<{gameId: *, path: *, playCount: *, name: *}>} + */ +async function getGame(gameId) { + const gameDoc = db.collection("games").doc(gameId); + return gameDoc.get() + .then(snapshot => { + const data = snapshot.data(); + if (!data) + return; // bad game; return nothing + return { + name: data.name, + shortDesc: data.shortDesc, + path: data.path, + imgPath: data.imgPath, + playCount: data.timesPlayed, + gameId: snapshot.id, + scoreOrder: data.scoreOrder, + }; + }) + .catch(err => { + console.log("Error getting documents", err); + }); +} + +/** + * Get the game scores for the given game. + * @author: jk + * @param {string} gameId The id of the game to get. + * @param {string} orderDir Direction to order. Either asc or desc. Defaults to asc. + */ +async function getGameScores(gameId, orderDir) { + if (orderDir !== "asc" && orderDir !== "desc") { + console.log("Bad sorting value. Defaulting to 'desc'"); + orderDir = "desc"; + } + const scoresQuery = db.collection(`games/${gameId}/scores`).orderBy("score", orderDir); + return Promise.all(await scoresQuery.get() + .then(snapshot => { + return snapshot.docs.map(async function(doc) { + const data = doc.data(); + if (!data) + return; // bad game; return nothing + return { + scoreId: doc.id, + score: data.score, + timestamp: data.timestamp, + user: await getUserData(data.user), + }; + }); + }) + .catch(err => { + console.log("Error getting Game documents", err); + })); +} + +/** + * Get the necessary user data from the given uid. + * @param uid The user id to get the data for. + * @returns {Promise<{uid: *, memberSince: *, name: *, email: *} | {uid: *, memberSince: string, name: string, email: string}>} + */ +async function getUserData(uid) { + return auth.getUser(uid) + .then(function(userRecord) { + // console.log('Successfully fetched user data:', userRecord.toJSON()); + return { + uid: uid, + name: userRecord.displayName, + email: userRecord.email, + memberSince: userRecord.metadata.creationTime, // creation time is already a string in GMT + } + }) + .catch(function(error) { + console.log(`Error fetching user data for ${uid}\n`, error); + return { + uid: uid, + name: "[deleted]", + email: "[deleted]", + memberSince: "[deleted]", + }; + }); +} + +/** + * Get necessary game data and sort it alphabetically. + * @author: jk + * @param games The list of all games with the full game data. (from getGames) + * @returns {Object[]} The sorted list of games with filtered data. + */ +function getGameDropdownData(games) { + return games.map(function(data) { + return { + name: data.name, + shortDesc: data.shortDesc, + gameId: data.gameId, + scoreOrder: data.scoreOrder, + }; + }).sort((a, b) => (a.name > b.name) ? 1 : -1); +} + +module.exports = { + getGames, + getGame, + getGameDropdownData, + getUserData, + getGameScores, +}; diff --git a/routes/src/pushData.js b/routes/src/pushData.js new file mode 100644 index 00000000..e5d79cff --- /dev/null +++ b/routes/src/pushData.js @@ -0,0 +1,39 @@ +const firebaseAdmin = require("firebase-admin"); +const db = firebaseAdmin.firestore(); +const auth = firebaseAdmin.auth(); + + +/** + * Create a score document in the score subcollection given of the game. + * @param gameId The id of the game the score is for. + * @param score The score. + * @param userId The uid of the player. + * @returns {Promise} The score document that was just created. + */ +function addScoreDefault(gameId, score, userId) { + return db.collection(`games/${gameId}/scores`).add({ + score, + user: userId, + timestamp: firebaseAdmin.firestore.Timestamp.now(), + }) + .then(function(docRef) { + // increment timesPlayed on parent + db.collection("games").doc(gameId).update({ + timesPlayed: firebaseAdmin.firestore.FieldValue.increment(1) + }) + .then(function() { + // console.log("Document successfully updated!"); + }) + .catch(function(error) { + console.error("Error updating document: ", error); + }); + return docRef; + }) + .catch(function(error) { + console.error("Error adding document: ", error); + }); +} + +module.exports = { + addScoreDefault, +}; \ No newline at end of file diff --git a/views/bdest.hbs b/views/bdest.hbs new file mode 100644 index 00000000..6353bf55 --- /dev/null +++ b/views/bdest.hbs @@ -0,0 +1,22 @@ +
+

BRICK DESTROYER!

+
+
+ +{{#section 'js'}} + + +{{/section}} + +{{#> layouts/modal-layout type="gameScore" title="Submit Score" submitText="Submit" }} + {{#*inline "form-content"}} + {{> includes/game-submit-default }} + {{/inline}} +{{/layouts/modal-layout}} + + +{{#> layouts/modal-layout type="help" title="Brick Destroyer Help" }} + {{#*inline "form-content"}} + {{> includes/help/bdest }} + {{/inline}} +{{/layouts/modal-layout}} \ No newline at end of file diff --git a/views/ex-game.hbs b/views/ex-game.hbs new file mode 100644 index 00000000..c4378d8d --- /dev/null +++ b/views/ex-game.hbs @@ -0,0 +1,32 @@ +
+

Epic Awesome Game

+ +{{!-- Game goes here --}} +

Please submit kindly

+ + +
+ +{{#section "css"}} + {{!-- Add CSS here --}} + +{{/section}} + +{{#section "js"}} +{{!-- Add JS here --}} + +{{/section}} + +{{#> layouts/modal-layout type="gameScore" title="Submit Score" submitText="Submit" }} + {{#*inline "form-content"}} + {{> includes/game-submit-default }} + {{/inline}} +{{/layouts/modal-layout}} + + +{{#> layouts/modal-layout type="help" title="Epic Awesome Help" }} + {{#*inline "form-content"}} + {{> includes/help/ex }} + {{/inline}} +{{/layouts/modal-layout}} + diff --git a/views/flappygoat.hbs b/views/flappygoat.hbs new file mode 100644 index 00000000..4217f8b3 --- /dev/null +++ b/views/flappygoat.hbs @@ -0,0 +1,33 @@ +
+

Flappy Goat

+ + {{!-- Game goes here --}} + + + +
+ +{{#section "css"}} +{{!-- Add CSS here --}} + {{!-- --}} +{{/section}} + +{{#section "js"}} +{{!-- Add JS here --}} + + {{!-- --}} +{{/section}} + +{{#> layouts/modal-layout type="gameScore" title="Submit Score" submitText="Submit" }} + {{#*inline "form-content"}} + {{> includes/game-submit-default }} + {{/inline}} +{{/layouts/modal-layout}} + + +{{#> layouts/modal-layout type="help" title="Flappy Bird Help" }} + {{#*inline "form-content"}} + {{> includes/help/flappygoat }} + {{/inline}} +{{/layouts/modal-layout}} + diff --git a/views/gompei.hbs b/views/gompei.hbs new file mode 100644 index 00000000..8c1b1c99 --- /dev/null +++ b/views/gompei.hbs @@ -0,0 +1,22 @@ +
+

Gompei Run

+
+
+ +{{#section 'js'}} + + +{{/section}} + +{{#> layouts/modal-layout type="gameScore" title="Submit Score" submitText="Submit" }} + {{#*inline "form-content"}} + {{> includes/game-submit-default }} + {{/inline}} +{{/layouts/modal-layout}} + + +{{#> layouts/modal-layout type="help" title="Gompei Run Help" }} + {{#*inline "form-content"}} + {{> includes/help/gompei }} + {{/inline}} +{{/layouts/modal-layout}} \ No newline at end of file diff --git a/views/helpers.js b/views/helpers.js new file mode 100644 index 00000000..8697ee98 --- /dev/null +++ b/views/helpers.js @@ -0,0 +1,16 @@ +const helpers = { + // allow any section to be defined in hbs using {{{_sections.section_name}}} + // this can then be fulfilled using {{#section 'css'}}Section html{{/section}} + section: function(name, options) { + if (!this._sections) this._sections = {}; + this._sections[name] = options.fn(this); + return null; + }, + // Take in a expression and dont return it as is. Allows have {{ }} in a rendered document without being evaluated + noeval: function(expression) { + return expression; + }, +}; + + +module.exports.helpers = helpers; \ No newline at end of file diff --git a/views/index.hbs b/views/index.hbs new file mode 100644 index 00000000..085d39e2 --- /dev/null +++ b/views/index.hbs @@ -0,0 +1,21 @@ +
+
+

Welcome

+ {{#each this.gameData as |entry|}} + + +
+

{{ name }}

+

{{shortDesc}}

+
+
+ {{/each}} +
+
+ + +{{#> layouts/modal-layout type="help" title="Help" }} + {{#*inline "form-content"}} + {{> includes/help/index }} + {{/inline}} +{{/layouts/modal-layout}} diff --git a/views/kenken.hbs b/views/kenken.hbs new file mode 100644 index 00000000..14890bba --- /dev/null +++ b/views/kenken.hbs @@ -0,0 +1,32 @@ +
+
+

Kenken

+

Time

+

You Won!

+
+ +
+
+
+ +{{#section 'css'}} +{{!-- Add CSS here --}} + +{{/section}} + +{{#section 'js'}} + +{{/section}} + +{{#> layouts/modal-layout type="gameScore" title="Submit Score" submitText="Submit" }} + {{#*inline "form-content"}} + {{> includes/game-submit-default }} + {{/inline}} +{{/layouts/modal-layout}} + + +{{#> layouts/modal-layout type="help" title="Kenken Help" }} + {{#*inline "form-content"}} + {{> includes/help/kenken }} + {{/inline}} +{{/layouts/modal-layout}} \ No newline at end of file diff --git a/views/layouts/layout.hbs b/views/layouts/layout.hbs new file mode 100644 index 00000000..fba18105 --- /dev/null +++ b/views/layouts/layout.hbs @@ -0,0 +1,119 @@ + + + + AJAJA Gaming + + + + + + {{{_sections.css}}} + + + + + + + + + + + + +{{> includes/nav-bar }} + +
+ {{{ body }}} +
+ + +{{#> layouts/modal-layout type="signIn" title="Login" submitText="Login" }} + {{#*inline "form-content"}} + {{> includes/auth-sign-in }} + {{/inline}} +{{/layouts/modal-layout}} +{{#> layouts/modal-layout type="signUp" title="Sign Up" submitText="Sign Up" }} + {{#*inline "form-content"}} + {{> includes/auth-sign-up }} + {{/inline}} +{{/layouts/modal-layout}} +{{> includes/scores }} + + + + + + + + + + +{{{_sections.js}}} +{{!-- auths shuld go before game --}} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/luck.hbs b/views/luck.hbs new file mode 100644 index 00000000..c8c7c9b7 --- /dev/null +++ b/views/luck.hbs @@ -0,0 +1,22 @@ +
+

Luck

+
+
+ +{{#section "js"}} + + +{{/section}} + +{{#> layouts/modal-layout type="gameScore" title="Submit Score" submitText="Submit" }} + {{#*inline "form-content"}} + {{> includes/game-submit-default }} + {{/inline}} +{{/layouts/modal-layout}} + + +{{#> layouts/modal-layout type="help" title="Luck Help" }} + {{#*inline "form-content"}} + {{> includes/help/luck }} + {{/inline}} +{{/layouts/modal-layout}} diff --git a/views/not-found.hbs b/views/not-found.hbs new file mode 100644 index 00000000..1369b5eb --- /dev/null +++ b/views/not-found.hbs @@ -0,0 +1,4 @@ +
+

Not Found

+

{{ msg }}

+
diff --git a/views/partials/includes/auth-sign-in.hbs b/views/partials/includes/auth-sign-in.hbs new file mode 100644 index 00000000..308c965e --- /dev/null +++ b/views/partials/includes/auth-sign-in.hbs @@ -0,0 +1,9 @@ + +
+ + + + +
diff --git a/views/partials/includes/auth-sign-up.hbs b/views/partials/includes/auth-sign-up.hbs new file mode 100644 index 00000000..33ffbf65 --- /dev/null +++ b/views/partials/includes/auth-sign-up.hbs @@ -0,0 +1,13 @@ + +
+ + +
+
+ + + + +
diff --git a/views/partials/includes/game-submit-default.hbs b/views/partials/includes/game-submit-default.hbs new file mode 100644 index 00000000..cd8f9506 --- /dev/null +++ b/views/partials/includes/game-submit-default.hbs @@ -0,0 +1,11 @@ + +
+

+ Score: {{ score }} +

+

+ Do you wish to submit this score? +

+
diff --git a/views/partials/includes/help/bdest.hbs b/views/partials/includes/help/bdest.hbs new file mode 100644 index 00000000..5766c5d6 --- /dev/null +++ b/views/partials/includes/help/bdest.hbs @@ -0,0 +1,12 @@ +

+ Brick Destroyer is a game where you destroy bricks with a ball. When the ball + reaches the bottom of the screen, a life will be lost. If the number of lives + reaches zero the game is over. + +

Controls:

+
    +
  • Space: serves the ball and restarts the game
  • +
  • Left Arrow Key: moves the paddle left
  • +
  • Right Arrow Key: moves the paddle right
  • +
+

\ No newline at end of file diff --git a/views/partials/includes/help/ex.hbs b/views/partials/includes/help/ex.hbs new file mode 100644 index 00000000..7b798875 --- /dev/null +++ b/views/partials/includes/help/ex.hbs @@ -0,0 +1 @@ +

You can submit whatever score you want, please be nice

\ No newline at end of file diff --git a/views/partials/includes/help/flappygoat.hbs b/views/partials/includes/help/flappygoat.hbs new file mode 100644 index 00000000..e391d9d0 --- /dev/null +++ b/views/partials/includes/help/flappygoat.hbs @@ -0,0 +1,4 @@ +

The game works the same way as regular old flappy bird.
+ Press space to initiate the game and also to jump through the pipes.
+ Get the highest score you can! +

\ No newline at end of file diff --git a/views/partials/includes/help/gompei.hbs b/views/partials/includes/help/gompei.hbs new file mode 100644 index 00000000..305f49fa --- /dev/null +++ b/views/partials/includes/help/gompei.hbs @@ -0,0 +1,13 @@ +

+ This game is inspired by the Google Chrome T-Rex game that you + can play when your browser cannot connect to the internet. + The point of the game is to help Gompei jump over rocks as long + you can to get the most points! +

Controls

+
    +
  • UP -> Jump (you can double jump!)
  • +
  • DOWN -> Fall faster when in midair
  • +
  • p -> Pause/resume game
  • +
+ Enjoy! +

\ No newline at end of file diff --git a/views/partials/includes/help/index.hbs b/views/partials/includes/help/index.hbs new file mode 100644 index 00000000..2d9306eb --- /dev/null +++ b/views/partials/includes/help/index.hbs @@ -0,0 +1,6 @@ +

+ Welcome to AJAJA Games!! Click any game you like to start playing. + If you hover over the game it will show a brief description. + If you wish to submit your scores please sign in. + You can make an account with us sign in with Google or GitHub. +

\ No newline at end of file diff --git a/views/partials/includes/help/kenken.hbs b/views/partials/includes/help/kenken.hbs new file mode 100644 index 00000000..d0066b43 --- /dev/null +++ b/views/partials/includes/help/kenken.hbs @@ -0,0 +1,7 @@ +

    +
  • The only numbers you may write are 1, 2, 3, 4, or 5.
  • +
  • No numbers may appear more than once in any row or column. (That is, all required numbers must appear once in every row and column.)
  • +
  • Each "cage" (region bounded by a heavy border) contains a "target number." If there's more than one cell in the cage, the target is also accompanied by an arithmetic operation. You must fill that cage with numbers that produce the target number, using only the specified arithmetic operation. Numbers may be repeated within a cage, if necessary, as long as they do not repeat within a single row or column.
  • +
  • In a one-cell cage, just write the target number in that cell.
  • +
The puzzle is constantly checked, you will be notified +once the correct board state is filled in.

\ No newline at end of file diff --git a/views/partials/includes/help/luck.hbs b/views/partials/includes/help/luck.hbs new file mode 100644 index 00000000..4639a2d2 --- /dev/null +++ b/views/partials/includes/help/luck.hbs @@ -0,0 +1,7 @@ +

+ Click on the pot of gold to start the game. + Each click will take a reading of your current luck percentage. + The game will end after ten readings. + After you can submit your score if you wish. + And also start a new game. +

\ No newline at end of file diff --git a/views/partials/includes/help/rockpaperscissors.hbs b/views/partials/includes/help/rockpaperscissors.hbs new file mode 100644 index 00000000..15e51f63 --- /dev/null +++ b/views/partials/includes/help/rockpaperscissors.hbs @@ -0,0 +1 @@ +

\ No newline at end of file diff --git a/views/partials/includes/help/sudoku.hbs b/views/partials/includes/help/sudoku.hbs new file mode 100644 index 00000000..0fca401c --- /dev/null +++ b/views/partials/includes/help/sudoku.hbs @@ -0,0 +1,5 @@ +

+ Sudoku is a number placing puzzle based on a 9x9 grid with several given numbers. The object is to place the numbers 1 to 9 in the empty squares so that each row, each column and each 3x3 box contains the same number only once. +

The puzzle is constantly checked, you will be notified + once the correct board state is filled in. +

\ No newline at end of file diff --git a/views/partials/includes/help/tower-defence.hbs b/views/partials/includes/help/tower-defence.hbs new file mode 100644 index 00000000..57ef7870 --- /dev/null +++ b/views/partials/includes/help/tower-defence.hbs @@ -0,0 +1,5 @@ +

+ Use the Sandbag, Machine Gun and Cannon towers to thwart the invading army. Dont let them reach the base! + Each tower costs money. You can earn money by destroying the enemy. + You can pause by pressing P. +

\ No newline at end of file diff --git a/views/partials/includes/help/user.hbs b/views/partials/includes/help/user.hbs new file mode 100644 index 00000000..4a287ee7 --- /dev/null +++ b/views/partials/includes/help/user.hbs @@ -0,0 +1,4 @@ +

+ This page has information about a specific user. + If you would like to get info about a user simply click on their name. +

\ No newline at end of file diff --git a/views/partials/includes/nav-bar.hbs b/views/partials/includes/nav-bar.hbs new file mode 100644 index 00000000..f1b3f6e1 --- /dev/null +++ b/views/partials/includes/nav-bar.hbs @@ -0,0 +1,41 @@ +{{!-- TODO: fix problem with realy thin windows and scrolling right --}} + diff --git a/views/partials/includes/scores-body-global.hbs b/views/partials/includes/scores-body-global.hbs new file mode 100644 index 00000000..d14569a0 --- /dev/null +++ b/views/partials/includes/scores-body-global.hbs @@ -0,0 +1,7 @@ +
+
    + {{#noeval "{{#each scoresData as |score|}}"}}{{/noeval}} +
  • {{#noeval '{{#add1 @index}}{{/add1}}. {{ user.name }} {{score.score}}'}}{{/noeval}}
  • + {{#noeval "{{/each}}"}}{{/noeval}} +
+
\ No newline at end of file diff --git a/views/partials/includes/scores-body-user.hbs b/views/partials/includes/scores-body-user.hbs new file mode 100644 index 00000000..49907185 --- /dev/null +++ b/views/partials/includes/scores-body-user.hbs @@ -0,0 +1,7 @@ +
+
    + {{#noeval "{{#each scoresData as |score|}}"}}{{/noeval}} +
  • {{#noeval '{{#add1 @index}}{{/add1}}. {{#ts}}{{/ts}} {{score.score}}'}}{{/noeval}}
  • + {{#noeval "{{/each}}"}}{{/noeval}} +
+
\ No newline at end of file diff --git a/views/partials/includes/scores.hbs b/views/partials/includes/scores.hbs new file mode 100644 index 00000000..c9c9a859 --- /dev/null +++ b/views/partials/includes/scores.hbs @@ -0,0 +1,54 @@ + \ No newline at end of file diff --git a/views/partials/layouts/modal-layout.hbs b/views/partials/layouts/modal-layout.hbs new file mode 100644 index 00000000..e54691b9 --- /dev/null +++ b/views/partials/layouts/modal-layout.hbs @@ -0,0 +1,32 @@ + \ No newline at end of file diff --git a/views/rockpaperscissors.hbs b/views/rockpaperscissors.hbs new file mode 100644 index 00000000..27c2f352 --- /dev/null +++ b/views/rockpaperscissors.hbs @@ -0,0 +1,67 @@ +
+
+

Rock Paper Scissors

+

Score: 0

+

Round: 1

+
+
+

Choose your move!

+
+
+ + + +
+
+
+
+ +{{#section 'css'}} +{{!-- Add CSS here --}} + +{{/section}} + +{{#section 'js'}} + +{{/section}} + +
+ + +
+ +{{#> layouts/modal-layout type="gameScore" title="Submit Score" submitText="Submit" }} + {{#*inline "form-content"}} + {{> includes/game-submit-default }} + {{/inline}} +{{/layouts/modal-layout}} + + +{{#> layouts/modal-layout type="help" title="Rock Paper Scissors Help" }} + {{#*inline "form-content"}} + {{> includes/help/rockpaperscissors }} + {{/inline}} +{{/layouts/modal-layout}} diff --git a/views/sudoku.hbs b/views/sudoku.hbs new file mode 100644 index 00000000..33c57899 --- /dev/null +++ b/views/sudoku.hbs @@ -0,0 +1,150 @@ +
+
+

Sudoku

+

Time

+

You Won!

+
+
5+
15X
3-
4-
20X
12X
12X
5
6+
1-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +{{#section 'css'}} +{{!-- Add CSS here --}} + +{{/section}} + +{{#section 'js'}} + +{{/section}} + +{{#> layouts/modal-layout type="gameScore" title="Submit Score" submitText="Submit" }} + {{#*inline "form-content"}} + {{> includes/game-submit-default }} + {{/inline}} +{{/layouts/modal-layout}} + + +{{#> layouts/modal-layout type="help" title="Sudoku Help" }} + {{#*inline "form-content"}} + {{> includes/help/sudoku }} + {{/inline}} +{{/layouts/modal-layout}} \ No newline at end of file diff --git a/views/tower-defence.hbs b/views/tower-defence.hbs new file mode 100644 index 00000000..6131671f --- /dev/null +++ b/views/tower-defence.hbs @@ -0,0 +1,37 @@ +
+

Open Tower Defence

+
+
+ +{{#section "js"}} + + + + + + + +{{/section}} + +{{#> layouts/modal-layout type="gameScore" title="Submit Score" submitText="Submit" }} + {{#*inline "form-content"}} + {{> includes/game-submit-default }} + {{/inline}} +{{/layouts/modal-layout}} + + +{{#> layouts/modal-layout type="help" title="Luck Help" }} + {{#*inline "form-content"}} + {{> includes/help/tower-defence }} + {{/inline}} +{{/layouts/modal-layout}} \ No newline at end of file diff --git a/views/user.hbs b/views/user.hbs new file mode 100644 index 00000000..bec5a545 --- /dev/null +++ b/views/user.hbs @@ -0,0 +1,15 @@ +
+

User Profile

+
    + {{#each this.userData as |data|}} +
  • {{ data }}
  • + {{/each}} +
+
+ + +{{#> layouts/modal-layout type="help" title="User Help" }} + {{#*inline "form-content"}} + {{> includes/help/user }} + {{/inline}} +{{/layouts/modal-layout}} \ No newline at end of file