Compare commits

..

No commits in common. "master" and "leaderboard-pagination" have entirely different histories.

25 changed files with 291 additions and 887 deletions

View file

@ -1,6 +1,5 @@
POSTGRES_PORT=5432
EXPRESS_PORT=3000
FRONTEND_PORT=8080
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres

183
README.md
View file

@ -1,30 +1,163 @@
# RaspberryRocketeer
## How to run
## Class Diagram of all classes
### Copy .env
First you need to copy the `.env.example`.
```mermaid
classDiagram
direction BT
class Collidable {
collides(o: Entity) boolean
}
class Database {
any _db
any db
}
class Entity {
constructor(position: Position, width: number, height: number, fill: number)
Position _position
number _width
number _height
number fill
boolean _showHitbox
update() void
draw() void
Position position
number width
number height
boolean showHitbox
}
class Game {
number id
number score
string playtime
Date date
number userId
}
class GamePgPromiseRepository {
insert(game: Game) Promise~Game~
serialize(raw: any) Game
}
class GameRepository {
insert(game: Game) Promise~Game~
}
class HighscoreLeaderboard
class HighscoreLeaderboardPgPromiseRepository {
getAll() Promise~HighscoreLeaderboard~
serialize(raw: any) HighscoreLeaderboard
}
class HighscoreLeaderboardRepository {
getAll() Promise~HighscoreLeaderboard~
}
class Leaderboard
class LeaderboardEntry {
number username
number rank
T score
}
class Obstacle {
constructor(position: Position, obstacleWidth: number, obstacleHeight: number, pipeImagePath: string)
Pipe pipeTop
Pipe pipeBottom
number padding
number speed
number _distanceBetweenPipes
number _startX
resetPosition() void
randomizeHeight() void
randomRange(min: number, max: number) number
update() void
draw() void
collides(o: Entity) boolean
any startX
any distanceBetweenPipes
}
class Pipe {
constructor(positionX: number, width: number, height: number)
any _image
update() void
draw() void
move(speed: number) void
collides(o: Entity) boolean
any image
}
class Position {
constructor(x: number, y: number)
number _x
number _y
number x
number y
}
class Raspberry {
constructor(image: string)
number lift
number gravity
number _velocity
any _image
Position position
number maxVelocity
number WIDTH
number HEIGHT
number FILL
update() void
applyGravity() void
forceBoundaries() void
boost() void
draw() void
number velocity
any image
}
class TimeLeaderboard
class TimeLeaderboardPgPromiseRepository {
getAll() Promise~TimeLeaderboard~
serialize(raw: any) TimeLeaderboard
}
class TimeLeaderboardRepository {
getAll() Promise~TimeLeaderboard~
}
class User {
number id
string name
}
class UserPgPromiseRepository {
getById(id: number) Promise~User~
getByName(name: string) Promise~User~
withIdExists(id: number) Promise~boolean~
withNameExists(name: string) Promise~boolean~
insert(user: Omit~User, "id"~) Promise~User~
serialize(raw: any) User
}
class UserRepository {
getById(id: number) Promise~User~
getByName(name: string) Promise~User~
withIdExists(userId: number) Promise~boolean~
withNameExists(username: string) Promise~boolean~
insert(user: Omit~User, "id"~) Promise~User~
}
class UserScores {
number userId
number highscore
number totalScore
string totalPlaytime
number averageScore
number gamesPlayed
}
class UserScoresPgPromiseRepository {
getById(id: number) Promise~UserScores~
serialize(raw: any) UserScores
}
class UserScoresRepository {
getById(userId: number) Promise~UserScores~
}
GamePgPromiseRepository --> GameRepository
HighscoreLeaderboardPgPromiseRepository --> HighscoreLeaderboardRepository
Obstacle ..> Collidable
Obstacle --> Entity
Pipe ..> Collidable
Pipe --> Entity
Raspberry --> Entity
TimeLeaderboardPgPromiseRepository --> TimeLeaderboardRepository
UserPgPromiseRepository --> UserRepository
UserScoresPgPromiseRepository --> UserScoresRepository
```shell
cp .env.example .env
```
<small>Note: It is recommended to change the values for the database user.</small>
### Install node packages
Go into the frontend folder using
```shell
cd frontend
```
and run:
```shell
npm install
```
### Start the container (in the project root)
```shell
docker compose up --build
```
You can then access the website on `localhost:8080`

View file

@ -9,9 +9,6 @@ export const gameRoute = express.Router()
gameRoute.use(express.json())
/**
* Test
*/
gameRoute.post(
'/add',
body('playtime')
@ -21,8 +18,6 @@ gameRoute.post(
body('userId')
.isInt({min: 1})
.custom(userWithIdExists),
body('score')
.isInt({min: 0}),
/**
* After processing the errors of express-validator, inserts the game into the DB
* @param req

View file

@ -27,7 +27,7 @@ leaderboardRoute.get('/highscore',
//endregion
const highscoreLeaderboardRepo: HighscoreLeaderboardRepository = new HighscoreLeaderboardPgPromiseRepository;
let highscoreLeaderboard: HighscoreLeaderboard;
if (req.query.pagination == true) {
if (req.query.pagination) {
const entriesPerPage = req.query.entriesPerPage;
const page = req.query.page;
highscoreLeaderboard = await highscoreLeaderboardRepo.getPage(entriesPerPage, page);
@ -53,7 +53,6 @@ leaderboardRoute.get('/totalplaytime',
*/
async (req, res) => {
try {
console.log(req.query)
//region validate parameters
const errors = validationResult(req);
if (!errors.isEmpty()) {
@ -62,8 +61,7 @@ leaderboardRoute.get('/totalplaytime',
//endregion
const timeLeaderboardRepo: TimeLeaderboardRepository = new TimeLeaderboardPgPromiseRepository;
let timeLeaderboard: TimeLeaderboard;
console.log(req.query)
if (req.query.pagination == true) {
if (req.query.pagination) {
const entriesPerPage = req.query.entriesPerPage;
const page = req.query.page;
timeLeaderboard = await timeLeaderboardRepo.getPage(entriesPerPage, page);

View file

@ -84,30 +84,3 @@ userRoute.get('/:userId/scores',
}
}
)
userRoute.get('/:name',
param('name')
.isString()
.isLength({min: 3, max: 32})
.matches(USERNAME_VALIDATION_REGEX),
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const name: string = req.params.name;
console.log(name)
try {
// get & return data
const userRepo: UserPgPromiseRepository = new UserPgPromiseRepository();
const user = await userRepo.getByName(name);
res.json(user);
} catch (error) {
// handle errors
console.log(error)
res.status(500).json({ errors: [{msg: "Internal server error"}]})
}
})

View file

@ -0,0 +1,40 @@
insert into "user" (name) values ('dpettus0');
insert into "user" (name) values ('egreetland1');
insert into "user" (name) values ('smontford2');
insert into "user" (name) values ('idagwell3');
insert into "user" (name) values ('lgagan4');
insert into "user" (name) values ('acarmont5');
insert into "user" (name) values ('kjermyn6');
insert into "user" (name) values ('dokieran7');
insert into "user" (name) values ('pdrinkel8');
insert into game (user_id, score, playtime, date) values ('1', 74, '19:59', '2022-07-19');
insert into game (user_id, score, playtime, date) values ('1', 86, '20:32', '2022-11-24');
insert into game (user_id, score, playtime, date) values ('1', 68, '10:41', '2022-03-24');
insert into game (user_id, score, playtime, date) values ('2', 39, '5:55', '2022-06-01');
insert into game (user_id, score, playtime, date) values ('2', 20, '9:23', '2022-03-12');
insert into game (user_id, score, playtime, date) values ('2', 28, '23:45', '2022-04-01');
insert into game (user_id, score, playtime, date) values ('2', 44, '18:43', '2022-06-24');
insert into game (user_id, score, playtime, date) values ('3', 92, '14:54', '2022-11-06');
insert into game (user_id, score, playtime, date) values ('3', 73, '0:45', '2022-07-26');
insert into game (user_id, score, playtime, date) values ('3', 27, '2:49', '2022-02-03');
insert into game (user_id, score, playtime, date) values ('4', 26, '2:32', '2022-07-19');
insert into game (user_id, score, playtime, date) values ('4', 12, '17:03', '2022-04-25');
insert into game (user_id, score, playtime, date) values ('4', 6, '8:49', '2021-12-03');
insert into game (user_id, score, playtime, date) values ('4', 22, '22:27', '2022-03-02');
insert into game (user_id, score, playtime, date) values ('5', 94, '1:04', '2022-10-19');
insert into game (user_id, score, playtime, date) values ('5', 2, '2:14', '2022-04-06');
insert into game (user_id, score, playtime, date) values ('5', 21, '17:18', '2022-06-03');
insert into game (user_id, score, playtime, date) values ('6', 33, '16:01', '2022-02-02');
insert into game (user_id, score, playtime, date) values ('6', 27, '7:03', '2022-02-06');
insert into game (user_id, score, playtime, date) values ('6', 62, '0:45', '2022-11-15');
insert into game (user_id, score, playtime, date) values ('7', 12, '8:54', '2022-06-29');
insert into game (user_id, score, playtime, date) values ('7', 63, '16:01', '2022-11-05');
insert into game (user_id, score, playtime, date) values ('7', 29, '0:46', '2022-10-01');
insert into game (user_id, score, playtime, date) values ('8', 67, '1:27', '2022-09-29');
insert into game (user_id, score, playtime, date) values ('8', 84, '10:37', '2021-12-18');
insert into game (user_id, score, playtime, date) values ('8', 14, '19:14', '2022-01-31');
insert into game (user_id, score, playtime, date) values ('9', 21, '19:04', '2022-03-08');
insert into game (user_id, score, playtime, date) values ('9', 46, '2:34', '2022-04-18');
insert into game (user_id, score, playtime, date) values ('9', 78, '9:33', '2022-09-10');
insert into game (user_id, score, playtime, date) values ('9', 82, '11:19', '2022-11-29');

View file

@ -1,32 +1,23 @@
version: '3.1'
services:
db:
build: backend/db
container_name: postgres-db
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- "${POSTGRES_PORT}:5432"
volumes:
- ./backend/pgdata:/var/lib/postgresql/data
api:
build: backend/api
depends_on:
- db
container_name: express-api
ports:
- "${EXPRESS_PORT}:3000"
vue:
build: frontend
container_name: frontend
ports:
- "${FRONTEND_PORT}:8080"
volumes:
- ./frontend:/app
version: '3.1'
services:
db:
build: backend/db
container_name: postgres-db
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- "${POSTGRES_PORT}:5432"
volumes:
- ./backend/pgdata:/var/lib/postgresql/data
api:
build: backend/api
depends_on:
- db
container_name: express-api
ports:
- "${EXPRESS_PORT}:3000"

View file

@ -1,3 +0,0 @@
node_modules
.gitignore
README.md

View file

@ -1,11 +0,0 @@
FROM node:18
COPY . /app
WORKDIR /app
RUN npm install
EXPOSE 8080
ENTRYPOINT ["npm", "run", "serve"]

View file

@ -8,7 +8,9 @@
"name": "raspberryrocketeer",
"version": "0.1.0",
"dependencies": {
"@types/p5": "^1.4.3",
"bootstrap": "^5.2.3",
"p5": "^1.5.0",
"vue": "^3.2.13"
},
"devDependencies": {
@ -829,6 +831,11 @@
"integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==",
"dev": true
},
"node_modules/@types/p5": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@types/p5/-/p5-1.4.3.tgz",
"integrity": "sha512-UjgBi1/VU0Dz1/JxzRZrDKlC54rMELWW+6oHx1pRKzKkVgwzvP+dEjZ1JbeMjK12VJMUjDx4lhGUmnUL2jBASQ=="
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/@types/parse-json/-/parse-json-4.0.0.tgz",
@ -6187,6 +6194,11 @@
"node": ">=6"
}
},
"node_modules/p5": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/p5/-/p5-1.5.0.tgz",
"integrity": "sha512-zZFMVUmGkXe2G5H6Sw7xsVhgdxMyEN/6SZnZqYdQ51513kTqPslLnukkwTbGf8YtW0RetTU0FTjYQMXnFD7KnQ=="
},
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmmirror.com/param-case/-/param-case-3.0.4.tgz",
@ -9869,6 +9881,11 @@
"integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==",
"dev": true
},
"@types/p5": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@types/p5/-/p5-1.4.3.tgz",
"integrity": "sha512-UjgBi1/VU0Dz1/JxzRZrDKlC54rMELWW+6oHx1pRKzKkVgwzvP+dEjZ1JbeMjK12VJMUjDx4lhGUmnUL2jBASQ=="
},
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/@types/parse-json/-/parse-json-4.0.0.tgz",
@ -14188,6 +14205,11 @@
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
"p5": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/p5/-/p5-1.5.0.tgz",
"integrity": "sha512-zZFMVUmGkXe2G5H6Sw7xsVhgdxMyEN/6SZnZqYdQ51513kTqPslLnukkwTbGf8YtW0RetTU0FTjYQMXnFD7KnQ=="
},
"param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmmirror.com/param-case/-/param-case-3.0.4.tgz",

View file

@ -9,12 +9,14 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@types/p5": "^1.4.3",
"bootstrap": "^5.2.3",
"vue": "^3.2.13",
"@vue/cli-service": "~5.0.0"
"p5": "^1.5.0",
"vue": "^3.2.13"
},
"devDependencies": {
"@vue/cli-plugin-typescript": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"typescript": "~4.5.5"
}
}

View file

@ -1,519 +0,0 @@
var __extends = (this && this.__extends) || (function(){
var extendStatics = function(d, b){
extendStatics = Object.setPrototypeOf ||
({__proto__: []} instanceof Array && function(d, b){
d.__proto__ = b;
}) ||
function(d, b){
for(var p in b) if(Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p];
};
return extendStatics(d, b);
};
return function(d, b){
if(typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __(){
this.constructor = d;
}
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var PIPE_IMAGE_PATH = "resources/dell-pc-min-min-small.png";
var BACKGROUND_IMAGE_PATH = "resources/htl-steyr-front.jpg";
var RASPBERRY_IMAGE_PATH = "resources/raspberry-rocket.png";
var FLOOR_IMAGE_PATH = "resources/table-min-min.png";
var FONT_PATH = "resources/PressStart2P-Regular.ttf";
var OBSTACLE_COUNT = 3;
var BOOST_KEYS = ["k", " "];
var floorHeight;
var obstacleWidth;
var obstacleOffset;
var backgroundImage;
var pipeImage;
var floorImage;
var font;
var obstacles = [];
var raspberry;
var startTime;
var playTime;
var score = 0;
var paused;
var hasAlreadyScored = false;
var hasDied = true;
var ready = true;
function preload(){
font = loadFont(FONT_PATH);
backgroundImage = loadImage(BACKGROUND_IMAGE_PATH);
pipeImage = loadImage(PIPE_IMAGE_PATH);
floorImage = loadImage(FLOOR_IMAGE_PATH);
}
function setup(){
createCanvas(1085, 600);
floorHeight = height / 5;
setupObstacleConsts();
setupFont();
setupGame();
var originalSetItem = localStorage.setItem;
localStorage.setItem = function(key, value){
var event = new Event('itemInserted');
event.value = value; // Optional..
event.key = key; // Optional..
window.dispatchEvent(event);
originalSetItem.apply(this, arguments);
};
}
function setupObstacleConsts(){
obstacleOffset = width / OBSTACLE_COUNT;
obstacleWidth = width / 22.727272727272727272;
Obstacle.distanceBetweenPipes = height / 2.5;
Obstacle.startX = width;
}
function setupFont(){
textSize(75);
textAlign(CENTER);
textFont(font);
}
function setupGame(){
paused = true;
raspberry = new Raspberry(RASPBERRY_IMAGE_PATH);
setupObstacles();
}
function setupObstacles(){
obstacles = [];
instantiateObstacles(OBSTACLE_COUNT);
obstacles.forEach(function(obstacle){
return obstacle.randomizeHeight();
});
}
function instantiateObstacles(number){
for(var i = 0; i < number; i++){
obstacles.push(new Obstacle(new Position(width + obstacleOffset * i, 0), obstacleWidth, height, pipeImage));
}
}
function draw(){
update();
gameLoop();
drawGame();
}
function drawGame(){
drawScenery();
drawEntities();
displayScore();
}
function drawScenery(){
background(backgroundImage);
drawFloor();
}
function drawFloor(){
push();
noFill();
image(floorImage, 0, height - floorHeight, width, floorHeight);
rect(0, height - floorHeight, width, floorHeight);
pop();
}
function drawEntities(){
raspberry.draw();
drawObstacles();
}
function drawObstacles(){
obstacles.forEach(function(obstacle){
obstacle.draw();
});
}
function gameLoop(){
if(!paused){
collisionCheck(obstacles[0]);
checkRaspberryScore();
}
}
function collisionCheck(o){
if(o.collides(raspberry)){
die();
setupGame();
}
}
function die(){
if(localStorage.getItem("frontend-ready") == "false")
return;
ready = false;
hasDied = true;
playTime = Date.now() - startTime;
exportToLocalStorage();
setTimeout(function(){
return ready = true;
}, 1000);
}
function exportToLocalStorage(){
localStorage.setItem("game-playTime", String(playTime));
localStorage.setItem("game-score", String(score));
localStorage.setItem("game-isRunning", String(!hasDied));
}
function displayScore(){
push();
fill(195, 33, 34);
text(score, 0, height / 8, width, height);
pop();
}
function update(){
if(!paused){
raspberry.update();
}
obstacles.forEach(function(obstacle){
if(!paused){
obstacle.update();
checkObstacleReset(obstacle);
}
});
}
function checkObstacleReset(obstacle){
if(obstacle.position.x < -obstacleWidth){
obstacle.resetPosition();
obstacles.shift();
obstacles.push(obstacle);
hasAlreadyScored = false;
}
}
function checkRaspberryScore(){
if((obstacles[0].position.x + obstacles[0].width / 2) < (raspberry.position.x + raspberry.width / 2)
&& !hasAlreadyScored){
score += 1;
hasAlreadyScored = true;
}
}
function resetScore(){
if(!hasDied || localStorage.getItem("frontend-ready") == "false")
return;
hasDied = false;
score = 0;
hasAlreadyScored = false;
startTime = Date.now();
exportToLocalStorage();
}
function keyPressed(){
if(!ready)
return;
if(BOOST_KEYS.includes(key.toLowerCase())){
resetScore();
raspberry.boost();
}
if(key == "Escape"){
paused = !paused;
} else if(paused){
paused = false;
}
}
var Entity = (function(){
function Entity(position, width, height, fill){
this.position = position;
this.width = width;
this.height = height;
this.fill = fill;
this._showHitbox = false;
}
Object.defineProperty(Entity.prototype, "position", {
get: function(){
return this._position;
},
set: function(value){
this._position = value;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Entity.prototype, "width", {
get: function(){
return this._width;
},
set: function(value){
this._width = value;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Entity.prototype, "height", {
get: function(){
return this._height;
},
set: function(value){
this._height = value;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Entity.prototype, "showHitbox", {
get: function(){
return this._showHitbox;
},
set: function(value){
this._showHitbox = value;
},
enumerable: false,
configurable: true
});
Entity.prototype.draw = function(){
push();
fill(this.fill);
rect(this.position.x, this.position.y, this.width, this.height);
pop();
};
return Entity;
}());
var Obstacle = (function(_super){
__extends(Obstacle, _super);
function Obstacle(position, obstacleWidth, obstacleHeight, image){
var _this = _super.call(this, position, obstacleWidth, obstacleHeight, 0) || this;
_this.speed = 3;
_this.padding = height / 6.6666666666666666;
_this.createPipes(position, obstacleHeight, obstacleWidth, image);
return _this;
}
Object.defineProperty(Obstacle, "startX", {
set: function(value){
this._startX = value;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Obstacle, "distanceBetweenPipes", {
set: function(value){
this._distanceBetweenPipes = value;
},
enumerable: false,
configurable: true
});
Obstacle.prototype.createPipes = function(position, obstacleHeight, obstacleWidth, pipeImage){
this.pipeTop = new Pipe(position.x, obstacleWidth, obstacleHeight, pipeImage);
this.pipeBottom = new Pipe(position.x, obstacleWidth, obstacleHeight, pipeImage);
};
Obstacle.prototype.resetPosition = function(){
this.randomizeHeight();
this.pipeBottom.position.x = Obstacle._startX;
this.pipeTop.position.x = Obstacle._startX;
};
Obstacle.prototype.randomizeHeight = function(){
this.pipeTop.height = this.randomRange(this.padding, height - this.padding - Obstacle._distanceBetweenPipes);
this.pipeBottom.position.y = this.pipeTop.height + Obstacle._distanceBetweenPipes;
this.pipeBottom.height = height - this.pipeTop.height - this.padding;
};
Obstacle.prototype.randomRange = function(min, max){
return Math.random() * (max - min) + min;
};
Obstacle.prototype.update = function(){
this.pipeTop.move(this.speed);
this.pipeBottom.move(this.speed);
this.position.x = this.pipeTop.position.x;
};
Obstacle.prototype.draw = function(){
this.pipeTop.draw();
this.pipeBottom.draw();
};
Obstacle.prototype.collides = function(o){
return this.pipeTop.collides(o) || this.pipeBottom.collides(o);
};
return Obstacle;
}(Entity));
var Pipe = (function(_super){
__extends(Pipe, _super);
function Pipe(positionX, width, height, image){
var _this = _super.call(this, new Position(positionX, 0), width, height, 0) || this;
_this.image = image;
return _this;
}
Pipe.prototype.update = function(){
};
Pipe.prototype.draw = function(){
push();
noFill();
var imageAspectRatio = this.image.height / this.image.width;
var computedImageHeight = imageAspectRatio * this.width;
this.drawImage(computedImageHeight, imageAspectRatio);
rect(this.position.x, this.position.y, this.width, this.height);
pop();
};
Pipe.prototype.drawImage = function(computedImageHeight, imageAspectRatio){
if(this.height > computedImageHeight){
var maxImageYPos = Math.ceil(this.height / computedImageHeight) * computedImageHeight;
for(var imageYPosition = 0; imageYPosition < maxImageYPos; imageYPosition += computedImageHeight){
if(imageYPosition + computedImageHeight >= maxImageYPos){
this.cropLastImage(imageYPosition, computedImageHeight, imageAspectRatio);
break;
}
image(this.image, this.position.x, this.position.y + imageYPosition, this.width, computedImageHeight);
}
} else{
image(this.image, this.position.x, this.position.y, this.width, this.height);
}
};
Pipe.prototype.cropLastImage = function(imageYPosition, computedImageHeight, imageAspectRatio){
var amountOfImages = Math.floor(imageYPosition / computedImageHeight);
var heightToEdge = this.height - (amountOfImages * computedImageHeight);
var croppedImage = this.image.get(0, 0, this.image.width, this.image.height - (heightToEdge * imageAspectRatio));
image(croppedImage, this.position.x, this.position.y + imageYPosition, this.width, heightToEdge);
};
Pipe.prototype.move = function(speed){
this.position.x -= speed;
};
Pipe.prototype.collides = function(o){
return this.position.x < (o.position.x + o.width) &&
(this.position.x + this.width) > o.position.x &&
this.position.y < (o.position.y + o.height) &&
(this.position.y + this.height) > o.position.y;
};
return Pipe;
}(Entity));
var Position = (function(){
function Position(x, y){
this._x = x;
this._y = y;
}
Object.defineProperty(Position.prototype, "x", {
get: function(){
return this._x;
},
set: function(value){
this._x = value;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Position.prototype, "y", {
get: function(){
return this._y;
},
set: function(value){
this._y = value;
},
enumerable: false,
configurable: true
});
return Position;
}());
var Raspberry = (function(_super){
__extends(Raspberry, _super);
function Raspberry(image){
var _this = this;
Raspberry.position = new Position(width / 6, height / 2);
Raspberry.height = height / 14.2857142857142857;
Raspberry.width = width / 11.1111111111111111;
_this = _super.call(this, Raspberry.position, Raspberry.width, Raspberry.height, Raspberry.FILL) || this;
_this.lift = -15;
_this.gravity = 0.45;
_this._velocity = 0;
Raspberry.bottomFloorOffset = (height / 5) - (height / 15 / 2);
_this.image = image;
return _this;
}
Object.defineProperty(Raspberry.prototype, "velocity", {
get: function(){
return this._velocity;
},
set: function(value){
this._velocity = (Math.abs(this.velocity) > Raspberry.maxVelocity) ? Raspberry.maxVelocity : value;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Raspberry.prototype, "image", {
get: function(){
return this._image;
},
set: function(path){
this._image = loadImage(path);
},
enumerable: false,
configurable: true
});
Raspberry.prototype.update = function(){
this.applyGravity();
this.forceBoundaries();
};
Raspberry.prototype.applyGravity = function(){
this.velocity += this.gravity;
this.position.y += this.velocity;
};
Raspberry.prototype.forceBoundaries = function(){
this.boundaryTop();
this.boundaryBottom();
};
Raspberry.prototype.boundaryTop = function(){
if(this.position.y < 0){
this.position.y = 0;
this.velocity = 0;
}
};
Raspberry.prototype.boundaryBottom = function(){
if(this.position.y + this.height + Raspberry.bottomFloorOffset > height){
this.position.y = height - this.height - Raspberry.bottomFloorOffset;
this.velocity = 0;
}
};
Raspberry.prototype.boost = function(){
this.velocity += this.lift;
};
Raspberry.prototype.draw = function(){
push();
noFill();
this.setPose();
this.drawObject();
pop();
};
Raspberry.prototype.drawObject = function(){
this.drawHitBox();
this.drawRocket();
};
Raspberry.prototype.drawRocket = function(){
image(this.image, 0, 0, this.width, this.height);
rect(0, 0, this.width, this.height);
};
Raspberry.prototype.drawHitBox = function(){
if(!this.showHitbox){
noStroke();
}
};
Raspberry.prototype.setPose = function(){
translate(this.position.x, this.position.y);
rotate((PI / 2) * (this.velocity / Raspberry.maxVelocity));
};
Raspberry.maxVelocity = 75;
Raspberry.FILL = 0;
return Raspberry;
}(Entity));
//# sourceMappingURL=build.js.map

View file

@ -10,8 +10,8 @@
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
<title><%= htmlWebpackPlugin.options.title %></title>
<script src="https://cdn.jsdelivr.net/npm/p5@1.5.0/lib/p5.js"></script>
<script src="./game.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5@1.5.0/lib/p5.js"></script>
</head>
<body style="background-color: beige;">
<noscript>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -4,17 +4,11 @@
<Header></Header>
</div>
<div class="row">
<UserScores :userScores="userScores"></UserScores>
<UserScores :userScores="userScores" ></UserScores>
</div>
<div class="row">
<Game :class="user ? '' : 'hidden'" :user-id="this.user?.id"
@gameFinished="this.updateUserScores()" ref="game">
<Game class="col">
</Game>
<Login v-if="!user" @userChange="(event) => {this.updateUser(event);}">
</Login>
<div v-if="user" class="logout-wrapper offset-10 col-2">
<RRButton @click="logOut()" text="Logout"></RRButton>
</div>
</div>
<div class="row">
<div class="col-4">
@ -28,7 +22,7 @@
</template>
<script lang="ts">
import {defineComponent} from 'vue';
import { defineComponent } from 'vue';
import Leaderboard from './components/Leaderboard.vue';
import UserScores from './components/UserScores.vue';
import Game from './components/Game.vue';
@ -36,16 +30,10 @@ import Header from './components/Header.vue';
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap/dist/js/bootstrap.min.js";
import Login from "@/components/Login.vue";
import {Rest} from "@/model/Rest";
import {User} from "@/model/User";
import RRButton from "@/components/RRButton.vue";
export default defineComponent({
name: 'App',
components: {
RRButton,
Login,
UserScores,
Leaderboard,
Game,
@ -54,42 +42,21 @@ export default defineComponent({
data() {
return {
userScores: {},
userId: -1,
user: null as User | null,
leaderboardEvent: new Event('reloadLeaderboard')
userId: 1,
}
},
created() {
localStorage.setItem("frontend-ready", "false");
},
methods: {
async fetchFromApi(path: string, method: "GET" | "POST") {
let res: Response = await fetch(Rest.URL + path, {method: method,});
return await res.json();
let res: Response = await fetch("http://localhost:3000" + path, {method: method,});
return await res.json();
},
async fetchUserScores() {
if (this.userId == -1) return;
return await this.fetchFromApi(`/user/${this.userId}/scores`, "GET");
},
async updateUserScores() {
this.userScores = await this.fetchUserScores();
},
async updateUser(user: User) {
if (user) {
this.user = user;
this.userId = user.id ?? -1;
await this.updateUserScores();
}
window.dispatchEvent(this.leaderboardEvent);
},
logOut(){
this.user = null;
this.userId = -1;
this.userScores = {};
localStorage.setItem('frontend-ready', 'false');
}
},
async created() {
this.userScores = await this.fetchUserScores();
}
});
</script>
@ -101,19 +68,10 @@ export default defineComponent({
}
.row {
margin-top: 2em;
margin-top:2em;
}
.everything {
margin-bottom: 4em;
}
.hidden {
visibility: hidden;
}
.logout-wrapper {
display: flex;
justify-content: right;
}
</style>

View file

@ -4,7 +4,7 @@
<script lang="ts">
export default {
name: "RRButton",
name: "Button",
props: {
text: {
type: String
@ -17,6 +17,5 @@ export default {
button {
border: 3px solid black;
background-color: beige;
width: auto;
}
</style>

View file

@ -1,66 +1,11 @@
<template>
<div class="wrapper">
<main></main>
</div>
<div class="wrapper">
<main></main>
</div>
</template>
<script>
import App from "@/App.vue";
import {Rest} from "@/model/Rest";
<script lang="ts">
export default {
name: "Game",
props: {
userId: null
},
emits: ['gameFinished'],
created() {
window.addEventListener('itemInserted', event => this.localStorageHandler(event), false);
},
methods: {
localStorageHandler(event) {
if (event.key !== 'game-isRunning') return;
if (event.value === 'false') { //means game is over
let playTime = this.msToHMS(Number(localStorage.getItem('game-playTime')));
let score = Number(localStorage.getItem('game-score'));
this.submitGame(score, playTime);
}
},
msToHMS(ms) {
// 1- Convert to seconds:
let seconds = ms / 1000;
// 2- Extract hours:
const hours = parseInt(String(seconds / 3600)); // 3,600 seconds in 1 hour
seconds = seconds % 3600; // seconds remaining after extracting hours
// 3- Extract minutes:
const minutes = parseInt(String(seconds / 60)); // 60 seconds in 1 minute
// 4- Keep only seconds not extracted to minutes:
seconds = seconds % 60;
return ((hours < 10) ? "0" : "") + hours + ":" + ((minutes < 10) ? "0" : "") + minutes + ":" + ((seconds < 10) ? "0" : "") + Math.floor(seconds);
},
async submitGame(score, playTime) {
let body = {
score: score,
playtime: playTime,
date: new Date().toISOString().substring(0, 10),
userId: this.userId,
}
let header = {
Accept: "application/json",
"Content-Type": "application/json",
};
await fetch(Rest.URL + '/game/add', {method: 'POST', body: JSON.stringify(body), headers: header});
this.$emit('gameFinished');
}
},
name: "Game"
}
</script>
<style>
#p5_loading {
display: none;
}
</style>
</script>

View file

@ -3,17 +3,14 @@
<div class="row">
<h3 class="col-10"><strong>{{ this.title() }}</strong></h3>
<div class="col-1">
<RRButton @click="prevPage" text="<"></RRButton>
<Button @click="prevPage" text="<"></Button>
</div>
<div class="col-1">
<RRButton @click="nextPage" text=">"></RRButton>
<Button @click="nextPage" text=">"></Button>
</div>
</div>
<div class="row" v-if="this.page.length > 0" v-for="entry in this.page" :key="entry.rank">
<LeaderboardEntry :entry="entry"></LeaderboardEntry>
</div>
<div class="row" v-else>
Loading...
<div class="row" v-for="entry in this.page" :key="entry.rank" >
<LeaderboardEntry :entry="entry" ></LeaderboardEntry>
</div>
</div>
</template>
@ -21,14 +18,13 @@
<script>
import LeaderboardEntry from "@/components/LeaderboardEntry.vue";
import RRButton from "@/components/RRButton.vue";
import {Rest} from "@/model/Rest";
import Button from "@/components/Button.vue";
export default {
name: "Leaderboard",
components: {
LeaderboardEntry,
RRButton,
Button,
},
data() {
return {
@ -38,47 +34,37 @@ export default {
}
},
props: {
type: "totalplaytime" | "highscore",
type: "totalplaytime" | "highscore",
},
created() {
this.updatePage();
window.addEventListener('itemInserted', event => this.onItemInserted(event), false);
},
updated() {
this.updatePage()
},
methods: {
async fetchPage() {
let res = await fetch(`${Rest.URL}/leaderboard/${this.type}?pagination=true&entriesPerPage=${this.entriesPerPage}&page=${this.pageNumber}`, {method: "GET"});
let res = await fetch(`http://localhost:3000/leaderboard/${this.type}?pagination=true&entriesPerPage=${this.entriesPerPage}&page=${this.pageNumber}`, {method: "GET"});
return await res.json();
},
title() {
return this.type === "totalplaytime" ? "Total Playtime" : "Highscore";
},
async nextPage() {
if (this.page.length !== this.entriesPerPage) return;
nextPage() {
this.pageNumber++;
await this.updatePage();
if (this.page.length === 0) {
this.prevPage();
}
this.updatePage();
},
prevPage() {
if (this.pageNumber <= 0) return;
this.pageNumber--;
if (this.pageNumber > 0) this.pageNumber--;
this.updatePage();
},
async updatePage() {
let tempPage = await this.fetchPage();
for (let i = 0; i < this.entriesPerPage; i++) {
this.page.pop();
this.page.pop()
}
for (const entry of tempPage) {
this.page.push(entry);
}
},
onItemInserted(event) {
if (event.key === 'game-isRunning' && event.value === 'false') {
this.updatePage();
this.page.push(entry)
}
}
}

View file

@ -1,18 +1,17 @@
<template>
<div class="col-1 text-right">{{this.entry.rank}}</div>
<span class="col offset-1 text-left username">{{this.entry.username}}</span>
<div class="col-2 text-right">{{this.entry.score}}</div>
<div class="col-1 text-left">{{this.entry.rank}}</div>
<div class="col text-left">{{this.entry.username}}</div>
<div class="col text-right">{{this.entry.score}}</div>
</template>
<script lang="ts">
import {defineComponent, PropType} from 'vue';
import {Leaderboard, LeaderboardEntry} from "@/model/Leaderboard";
import {defineComponent} from 'vue';
export default defineComponent({
name: "LeaderboardEntry",
props: {
entry: {
type: Object as PropType<LeaderboardEntry<string>>,
type: Object,
default: () => ({}),
},
},
@ -23,9 +22,4 @@ export default defineComponent({
* {
font-size: 1.5em;
}
.username {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>

View file

@ -1,65 +0,0 @@
<template>
<h2>Enter a username</h2>
<div class="form-floating mb-3">
<input class="form-control" id="floatingInput" placeholder="example name" v-model="username">
<label for="floatingInput">Username</label>
<RRButton @click="setUser()" text="Confirm"></RRButton>
</div>
</template>
<script>
import {User} from "@/model/User";
import RRButton from "@/components/RRButton.vue";
export default {
name: "Login",
components: {
RRButton
},
data() {
return {
username: '',
}
},
emits: ['userChange'],
methods: {
async setUser() {
if (this.username === '') return;
let user;
user = await User.getByName(this.username);
if(user.errors){
user = await User.create(this.username);
}
if(user.errors){
console.error("Something when wrong when logging in, please contact admin!")
return;
}
if (user) {
this.$emit('userChange', user);
}
},
}
}
</script>
<style scoped>
input {
border: 3px solid black;
border-radius: 0;
background-color: beige;
margin-bottom: 5px;
}
input:focus {
background: beige;
border-color: rgba(184,134,11, 0.8);
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px rgba(184,134,11, 0.6);
}
label {
margin-left: 10px;
}
</style>

View file

@ -1,3 +0,0 @@
export class Rest {
static readonly URL = 'http://localhost:3000';
}

View file

@ -1,34 +1,4 @@
import {Rest} from "@/model/Rest";
export class User {
id?: number;
name?: string;
constructor(id: number, name: string) {
this.name = name;
}
static async getByName(name: string): Promise<User> {
let res: Response = await fetch(Rest.URL + '/user/' + name, {method: 'GET'});
return await res.json();
}
static async create(name: string): Promise<User> {
let body = {
name: name
};
let header = {
Accept: "application/json",
"Content-Type": "application/json",
};
let res: Response = await fetch( Rest.URL + '/user/register', {
method: 'POST',
body: JSON.stringify(body),
headers: header,
});
return await res.json();
}
export interface User {
id?: number,
name: string,
}