diff --git a/.idea/RaspberryRocketeer.iml b/.idea/RaspberryRocketeer.iml index 954f68e..822a58f 100644 --- a/.idea/RaspberryRocketeer.iml +++ b/.idea/RaspberryRocketeer.iml @@ -5,5 +5,6 @@ + \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md index 146fbaf..8343dd3 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -2,32 +2,47 @@ classDiagram Entity <|-- Raspberry Entity <|-- Obstacle -Entity: -Position position -Entity: -int width -Entity: -int height -Entity: -update() -Entity: -draw() +Entity <|-- Pipe +Entity: -number fill +Entity: +Position position +Entity: +number width +Entity: +number height +Entity: +abstract update() +Entity: +draw() Entity: -detectCollision(Entity other) + class Raspberry{ --input() +-number lift +-number gravity +-static number maxVelocity ++number velocity + +-applyGravity() +-forceBoundaries() -boost() ++update() } class Obstacle{ -Entity pipeTop -Entity pipeBottom --int distanceBetweenPipes --int padding --resetPosition() +-number distanceBetweenPipes +-number padding +-number speed +-static number startX + +-randomRange() ++resetPosition() ++update() ++draw() +} + +class Pipe { ++update() } class Position{ --int _x --int _y - -+get x() -+set x() -+get y() -+set y() ++int x ++int y } ``` \ No newline at end of file diff --git a/frontend/game.ts b/frontend/game.ts index 5f03dc3..07fde06 100644 --- a/frontend/game.ts +++ b/frontend/game.ts @@ -1,5 +1,128 @@ +const pipeImagePath: string = "resources/raspberry-low-res.png"; +const obstacleWidth: number = 88; +let obstacleOffset: number; +const backgroundImagePath: string = "resources/raspberry-low-res.png"; +let backgroundImage: any; +const raspberryImagePath: string = "resources/raspberry-rocket.png"; + +let obstacles: Obstacle[] = []; +let raspberry: Raspberry; +let paused: boolean; +let score: number; +let hasAlreadyScored: boolean; + function setup() { - createCanvas(400, 400) - background(187) - line(0,0, 400,400) + backgroundImage = loadImage(backgroundImagePath); + createCanvas(2000, 1000); + obstacleOffset = width / 3; + + textSize(150); + textFont("resources/JetBrains-Mono-Regular.ttf"); + + setupGame(); +} + +/** + * Sets up everything needed for the game + */ +function setupGame() { + paused = true; + + score = 0; + raspberry = new Raspberry(); + raspberry.image = raspberryImagePath; + + // Create all obstacles + obstacles = []; + obstacles.push(new Obstacle( + new Position(width, 0), + obstacleWidth, + height, + pipeImagePath, + )); + obstacles.push(new Obstacle( + new Position(width + obstacleOffset, 0), + obstacleWidth, + height, + pipeImagePath, + )); + obstacles.push(new Obstacle( + new Position(width + obstacleOffset * 2, 0), + obstacleWidth, + height, + pipeImagePath, + )); + + // Randomize position of all Obstacles + obstacles.forEach((obstacle) => obstacle.randomizeHeight()); +} + +function draw() { + background(backgroundImage) + if (!paused) { + raspberry.update(); + } + raspberry.draw(); + + // Reset Obstacles Position + obstacles.forEach((obstacle) => { + if (!paused) { + obstacle.update(); + checkObstacleReset(obstacle); + } + + obstacle.draw(); + }); + + // Check for collisions with pipes and set score + if (!paused) { + if (obstacles[0].collides(raspberry)) { + setupGame(); + } + checkRaspberryScore(); + obstacles[0].draw(); + } + + push(); + fill(200, 100, 60); + text(score, width / 2, height / 10, width, height); + pop(); +} + +/** + * Check if obstacle positions should be reset and reset if so + * @param obstacle obstacle to check + */ +function checkObstacleReset(obstacle: Obstacle) { + if (obstacle.position.x < -obstacleWidth) { + obstacle.resetPosition(); + obstacles.shift(); + obstacles.push(obstacle); + hasAlreadyScored = false; + } +} + +/** + * Check if the raspberry should score and set score + */ +function checkRaspberryScore() { + if ((obstacles[0].position.x + obstacles[0].width / 2) < (raspberry.position.x + raspberry.width / 2) + && !hasAlreadyScored) { + score += 1; + hasAlreadyScored = true; + } +} + +function keyPressed() { + // Jump + if (key.toLowerCase() == "k") { + raspberry.boost(); + } + + // Pause the Game + if (key == "Escape") { + paused = !paused; + } else if (paused) { + paused = false; + } } \ No newline at end of file diff --git a/frontend/models/Collidable.ts b/frontend/models/Collidable.ts new file mode 100644 index 0000000..46f46d9 --- /dev/null +++ b/frontend/models/Collidable.ts @@ -0,0 +1,7 @@ +interface Collidable { + /** + * Determines when two entities collide + * @param o other entity + */ + collides(o: Entity): boolean; +} \ No newline at end of file diff --git a/frontend/models/Entity.ts b/frontend/models/Entity.ts new file mode 100644 index 0000000..745f71a --- /dev/null +++ b/frontend/models/Entity.ts @@ -0,0 +1,65 @@ +abstract class Entity { + private _position: Position; + private _width: number; + private _height: number; + private fill: number; + private _showHitbox: boolean; + + //region Getter & Setter + get position(): Position { + return this._position; + } + + set position(value: Position) { + this._position = value; + } + + get width(): number { + return this._width; + } + + set width(value: number) { + this._width = value; + } + + get height(): number { + return this._height; + } + + set height(value: number) { + this._height = value; + } + + get showHitbox(): boolean { + return this._showHitbox; + } + + set showHitbox(value: boolean) { + this._showHitbox = value; + } + //endregion + + /** + * Constructs the Entity + * @param position starting Position + * @param width entity width + * @param height entity height + * @param fill fill color + */ + protected constructor(position: Position, width: number, height: number, fill: number) { + this.position = position; + this.width = width; + this.height = height; + this.fill = fill; + this._showHitbox = false; + } + + public abstract update(): void; + + public draw(): void { + push(); + fill(this.fill); + rect(this.position.x, this.position.y, this.width, this.height); + pop(); + } +} diff --git a/frontend/models/Obstacle.ts b/frontend/models/Obstacle.ts new file mode 100644 index 0000000..e12ad11 --- /dev/null +++ b/frontend/models/Obstacle.ts @@ -0,0 +1,77 @@ +class Obstacle extends Entity implements Collidable { + private pipeTop: Pipe; + private pipeBottom: Pipe; + private readonly distanceBetweenPipes: number; + private readonly padding: number = 150; + private readonly speed: number = 3; + + private static startX: number; + + /** + * Constructs the Obstacle with the given image + * (fill is not used here) + * @param position starting position of the obstacle + * @param obstacleWidth width of the obstacle + * @param obstacleHeight height of the obstacle + * @param pipeImagePath path to the image to be used + */ + constructor(position: Position, obstacleWidth: number, obstacleHeight: number, pipeImagePath: string) { + super(position, obstacleWidth, obstacleHeight, 0); + this.pipeTop = new Pipe(position.x, obstacleWidth, obstacleHeight); + this.pipeBottom = new Pipe(position.x, obstacleWidth, obstacleHeight); + this.pipeTop.image = pipeImagePath; + this.pipeBottom.image = pipeImagePath; + + this.distanceBetweenPipes = height / 2.5; + Obstacle.startX = width; + } + + /** + * Resets the position of the obstacle to the Obstacle.startX variable + * Randomises the height of the pipes using the padding variable + */ + public resetPosition(): void { + this.randomizeHeight(); + + this.pipeBottom.position.x = Obstacle.startX; + this.pipeTop.position.x = Obstacle.startX; + } + + /** + * Randomizes the height of the pipes + */ + public randomizeHeight(): void { + this.pipeTop.height = this.randomRange(this.padding, height - this.padding - this.distanceBetweenPipes); + this.pipeTop.position.y = 0; + this.pipeBottom.position.y = this.pipeTop.height + this.distanceBetweenPipes; + this.pipeBottom.height = height - this.pipeTop.height - this.padding; + } + + /** + * Creates a random number between the min and max parameter + * @param min minimum number + * @param max maximum number + */ + private randomRange(min: number, max: number): number { + return Math.random() * (max - min) + min; + } + + public update(): void { + this.pipeTop.position.x -= this.speed; + this.pipeBottom.position.x -= this.speed; + this.position.x = this.pipeTop.position.x; + } + + public draw(): void { + this.pipeTop.draw(); + this.pipeBottom.draw(); + } + + /** + * Determines when the obstacle is colliding with another entity + * @param o other entity + */ + public collides(o: Entity): boolean { + return this.pipeTop.collides(o) || this.pipeBottom.collides(o); + } +} \ No newline at end of file diff --git a/frontend/models/Pipe.ts b/frontend/models/Pipe.ts new file mode 100644 index 0000000..26e402a --- /dev/null +++ b/frontend/models/Pipe.ts @@ -0,0 +1,50 @@ +class Pipe extends Entity implements Collidable { + private _image: any; + + //region Getter & Setter + get image(): any { + return this._image; + } + + set image(path: string) { + this._image = loadImage(path); + } + //endregion + + /** + * Constructs the pipe + * @param positionX starting x-Position + * @param width pipe width + * @param height pipe height + */ + constructor(positionX: number, width: number, height: number) { + super(new Position(positionX, 0), width, height, 0); + } + + public update(): void { + } + + public draw(): void { + push(); + image(this.image, this.position.x, this.position.y, this.width, this.height); + noFill(); + rect( + this.position.x, + this.position.y, + this.width, + this.height + ); + pop(); + } + + /** + * Determines when the pipe is colliding with another entity + * @param o other entity + */ + collides(o: Entity): boolean { + return this.position.x < (o.position.x + o.width) && //inside left border + (this.position.x + this.width) > o.position.x && //but not outside right border + this.position.y < (o.position.y + o.height) && //inside top border + (this.position.y + this.height) > o.position.y; //but not outside bottom border + } +} \ No newline at end of file diff --git a/frontend/models/Position.ts b/frontend/models/Position.ts new file mode 100644 index 0000000..58b58ae --- /dev/null +++ b/frontend/models/Position.ts @@ -0,0 +1,32 @@ +class Position { + private _x: number; + private _y: number; + + //region Getter & Setter + get x(): number { + return this._x; + } + + set x(value: number) { + this._x = value; + } + + get y(): number { + return this._y; + } + + set y(value: number) { + this._y = value; + } + //endregion + + /** + * Constructs the position + * @param x x-Position + * @param y y-Position + */ + constructor(x: number, y: number) { + this._x = x; + this._y = y; + } +} \ No newline at end of file diff --git a/frontend/models/Raspberry.ts b/frontend/models/Raspberry.ts new file mode 100644 index 0000000..32fcb3e --- /dev/null +++ b/frontend/models/Raspberry.ts @@ -0,0 +1,80 @@ +class Raspberry extends Entity { + private readonly lift: number = -20; + private readonly gravity: number = 1.314159265358979323846264338; + private _velocity: number = 0; + private _image: any; + private static readonly maxVelocity: number = 100; + + //region Getter & Setter + get velocity(): number { + return this._velocity; + } + + set velocity(value: number) { + this._velocity = (Math.abs(this.velocity) > Raspberry.maxVelocity) ? Raspberry.maxVelocity : value; + } + + get image(): any { + return this._image; + } + + set image(path: string) { + this._image = loadImage(path); + } + + //endregion + + /** + * Constructs the Raspberry with fixed sizes + */ + constructor() { + super(new Position(width / 6, height / 2), 180, 70, 0); + } + + public update(): void { + this.applyGravity(); + this.forceBoundaries(); + } + + /** + * Lets the Raspberry fall to the ground + */ + private applyGravity(): void { + this.velocity += this.gravity; + this.position.y += this.velocity; + } + + private forceBoundaries(): void { + if (this.position.y + this.height > height) { + this.position.y = height - this.height; + this.velocity = 0; + } + + if (this.position.y < 0) { + this.position.y = 0; + this.velocity = 0; + } + } + + public boost(): void { + this.velocity += this.lift; + } + + public draw(): void { + push(); + noFill(); + translate(this.position.x, this.position.y); + rotate((PI / 2) * (this.velocity / Raspberry.maxVelocity)); + image(this.image, 0, 0, this.width, this.height); + if (!this.showHitbox) { + noStroke(); + } + rect( + 0, + 0, + this.width, + this.height + ); + pop(); + } +} \ No newline at end of file diff --git a/frontend/resources/JetBrains-Mono-Regular.ttf b/frontend/resources/JetBrains-Mono-Regular.ttf new file mode 100644 index 0000000..0aea9e7 Binary files /dev/null and b/frontend/resources/JetBrains-Mono-Regular.ttf differ diff --git a/frontend/resources/raspberry-low-res.png b/frontend/resources/raspberry-low-res.png new file mode 100644 index 0000000..d3f9dae Binary files /dev/null and b/frontend/resources/raspberry-low-res.png differ diff --git a/frontend/resources/raspberry-rocket.png b/frontend/resources/raspberry-rocket.png new file mode 100644 index 0000000..5ff61e7 Binary files /dev/null and b/frontend/resources/raspberry-rocket.png differ diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 2ff3f78..ff99ffb 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { - "module": "none", "noImplicitAny": true, "outFile": "./build/build.js", "preserveConstEnums": true, @@ -8,6 +7,11 @@ "rootDir": ".", "sourceMap": true, "target": "es5", - "moduleResolution": "node" + "moduleResolution": "node", + "lib": [ + "dom", + "es5", + "scripthost" + ] } } \ No newline at end of file