diff --git a/frontend/src/App.css b/frontend/src/App.css
index b9d355d..2e4abcd 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -1,7 +1,7 @@
#root {
- max-width: 1280px;
+ /* max-width: 1280px; */
margin: 0 auto;
- padding: 2rem;
+ /* padding: 2rem; */
text-align: center;
}
@@ -40,3 +40,34 @@
.read-the-docs {
color: #888;
}
+
+.stack-container {
+ margin-top: 20px;
+ margin-left: 20px;
+ padding: 20px;
+ border: 2px solid white;
+ border-top: none;
+ max-height: calc(6 * (60px));
+ width: 120px;
+}
+
+.stack-list {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+
+.stack-item {
+ padding: 10px 15px;
+ border-radius: 5px;
+ margin-bottom: 10px;
+ color: white;
+ font-weight: bold;
+ background-image: linear-gradient(to bottom, orange, red);
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ transition: background-color 0.5s ease;
+}
+
+.stack-item:hover {
+ background-image: linear-gradient(to bottom, rgb(192, 125, 0), rgb(196, 0, 0));
+}
diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx
index b5563b1..317f2f1 100644
--- a/frontend/src/components/Footer.tsx
+++ b/frontend/src/components/Footer.tsx
@@ -1,4 +1,5 @@
import { FaGithub, FaTwitter, FaYoutube } from "react-icons/fa";
+import { FaXTwitter } from "react-icons/fa6";
import { Link } from "react-router-dom";
export const Footer = () => {
@@ -31,7 +32,7 @@ export const Footer = () => {
-
+
diff --git a/frontend/src/game/classes/Ball.ts b/frontend/src/game/classes/Ball.ts
index fb58e43..62f22b5 100644
--- a/frontend/src/game/classes/Ball.ts
+++ b/frontend/src/game/classes/Ball.ts
@@ -1,77 +1,102 @@
import { gravity, horizontalFriction, verticalFriction } from "../constants";
import { Obstacle, Sink } from "../objects";
import { pad, unpad } from "../padding";
+import { Wave } from "./Wave";
export class Ball {
- private x: number;
- private y: number;
- private radius: number;
- private color: string;
- private vx: number;
- private vy: number;
- private ctx: CanvasRenderingContext2D;
- private obstacles: Obstacle[]
- private sinks: Sink[]
- private onFinish: (index: number) => void;
+ private x: number;
+ private y: number;
+ private radius: number;
+ private color: string;
+ private vx: number;
+ private vy: number;
+ private ctx: CanvasRenderingContext2D;
+ private obstacles: Obstacle[];
+ private sinks: Sink[];
+ private onFinish: (index: number) => void;
+ private waves: Wave[]; // Added waves property to keep track of wave instances
- constructor(x: number, y: number, radius: number, color: string, ctx: CanvasRenderingContext2D, obstacles: Obstacle[], sinks: Sink[], onFinish: (index: number) => void) {
- this.x = x;
- this.y = y;
- this.radius = radius;
- this.color = color;
- this.vx = 0;
- this.vy = 0;
- this.ctx = ctx;
- this.obstacles = obstacles;
- this.sinks = sinks;
- this.onFinish = onFinish;
- }
-
- draw() {
- this.ctx.beginPath();
- this.ctx.arc(unpad(this.x), unpad(this.y), this.radius, 0, Math.PI * 2);
- this.ctx.fillStyle = this.color;
- this.ctx.fill();
- this.ctx.closePath();
- }
-
- update() {
- this.vy += gravity;
- this.x += this.vx;
- this.y += this.vy;
-
- // Collision with obstacles
- this.obstacles.forEach(obstacle => {
- const dist = Math.hypot(this.x - obstacle.x, this.y - obstacle.y);
- if (dist < pad(this.radius + obstacle.radius)) {
- // Calculate collision angle
- const angle = Math.atan2(this.y - obstacle.y, this.x - obstacle.x);
- // Reflect velocity
- const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
- this.vx = (Math.cos(angle) * speed * horizontalFriction);
- this.vy = Math.sin(angle) * speed * verticalFriction;
-
- // Adjust position to prevent sticking
- const overlap = this.radius + obstacle.radius - unpad(dist);
- this.x += pad(Math.cos(angle) * overlap);
- this.y += pad(Math.sin(angle) * overlap);
- }
- });
-
- // Collision with sinks
- for (let i = 0; i < this.sinks.length; i++) {
- const sink = this.sinks[i];
- if (
- unpad(this.x) > sink.x - sink.width / 2 &&
- unpad(this.x) < sink.x + sink.width / 2 &&
- (unpad(this.y) + this.radius) > (sink.y - sink.height / 2)
- ) {
- this.vx = 0;
- this.vy = 0;
- this.onFinish(i);
- break;
- }
+ constructor(
+ x: number,
+ y: number,
+ radius: number,
+ color: string,
+ ctx: CanvasRenderingContext2D,
+ obstacles: Obstacle[],
+ sinks: Sink[],
+ onFinish: (index: number) => void
+ ) {
+ this.x = x;
+ this.y = y;
+ this.radius = radius;
+ this.color = color;
+ this.vx = 0;
+ this.vy = 0;
+ this.ctx = ctx;
+ this.obstacles = obstacles;
+ this.sinks = sinks;
+ this.onFinish = onFinish;
+ this.waves = []; // Initialize waves array
+ }
+
+ draw() {
+ this.ctx.beginPath();
+ this.ctx.arc(unpad(this.x), unpad(this.y), this.radius, 0, Math.PI * 2);
+ this.ctx.fillStyle = this.color;
+ this.ctx.fill();
+ this.ctx.closePath();
+ }
+
+ update() {
+ this.vy += gravity;
+ this.x += this.vx;
+ this.y += this.vy;
+
+ // Collision with obstacles
+ this.obstacles.forEach((obstacle) => {
+ const dist = Math.hypot(this.x - obstacle.x, this.y - obstacle.y);
+ if (dist < pad(this.radius + obstacle.radius)) {
+ // Calculate collision angle
+ const angle = Math.atan2(this.y - obstacle.y, this.x - obstacle.x);
+ // Reflect velocity
+ const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
+ this.vx = Math.cos(angle) * speed * horizontalFriction;
+ this.vy = Math.sin(angle) * speed * verticalFriction;
+
+ // Adjust position to prevent sticking
+ const overlap = this.radius + obstacle.radius - unpad(dist);
+ this.x += pad(Math.cos(angle) * overlap);
+ this.y += pad(Math.sin(angle) * overlap);
+
+ // Create and start a new wave
+ this.waves.push(
+ new Wave(unpad(obstacle.x), unpad(obstacle.y), this.ctx)
+ ); // Add wave to waves array
+ }
+ });
+
+ // Collision with sinks
+ for (let i = 0; i < this.sinks.length; i++) {
+ const sink = this.sinks[i];
+ if (
+ unpad(this.x) > sink.x - sink.width / 2 &&
+ unpad(this.x) < sink.x + sink.width / 2 &&
+ unpad(this.y) + this.radius > sink.y - sink.height / 2
+ ) {
+ this.vx = 0;
+ this.vy = 0;
+ this.onFinish(i);
+ break;
}
}
-
- }
\ No newline at end of file
+
+ // Update and remove finished waves
+ this.waves.forEach((wave, index) => {
+ wave.update();
+ wave.draw();
+ if (wave.isDone()) {
+ this.waves.splice(index, 1);
+ }
+ });
+ }
+}
diff --git a/frontend/src/game/classes/BallManager.ts b/frontend/src/game/classes/BallManager.ts
index de49019..fc8f400 100644
--- a/frontend/src/game/classes/BallManager.ts
+++ b/frontend/src/game/classes/BallManager.ts
@@ -1,94 +1,177 @@
-import { HEIGHT, WIDTH, ballRadius, obstacleRadius, sinkWidth } from "../constants";
+import {
+ HEIGHT,
+ WIDTH,
+ ballRadius,
+ obstacleRadius,
+ sinkWidth,
+} from "../constants";
import { Obstacle, Sink, createObstacles, createSinks } from "../objects";
import { pad, unpad } from "../padding";
import { Ball } from "./Ball";
export class BallManager {
- private balls: Ball[];
- private canvasRef: HTMLCanvasElement;
- private ctx: CanvasRenderingContext2D;
- private obstacles: Obstacle[]
- private sinks: Sink[]
- private requestId?: number;
- private onFinish?: (index: number,startX?: number) => void;
+ private balls: Ball[];
+ private canvasRef: HTMLCanvasElement;
+ private ctx: CanvasRenderingContext2D;
+ private obstacles: Obstacle[];
+ private sinks: Sink[];
+ private requestId?: number;
+ private onFinish?: (
+ index: number,
+ startX?: number,
+ multiplier?: number
+ ) => void;
+ private stack: number[]; // Stack to store the last 6 entries
- constructor(canvasRef: HTMLCanvasElement, onFinish?: (index: number,startX?: number) => void) {
- this.balls = [];
- this.canvasRef = canvasRef;
- this.ctx = this.canvasRef.getContext("2d")!;
- this.obstacles = createObstacles();
- this.sinks = createSinks();
- this.update();
- this.onFinish = onFinish;
- }
+ constructor(
+ canvasRef: HTMLCanvasElement,
+ onFinish?: (index: number, startX?: number, multiplier?: number) => void
+ ) {
+ this.balls = [];
+ this.canvasRef = canvasRef;
+ this.ctx = this.canvasRef.getContext("2d")!;
+ this.obstacles = createObstacles();
+ this.sinks = createSinks();
+ this.update();
+ this.onFinish = onFinish;
+ this.stack = [];
+ }
- addBall(startX?: number) {
- const newBall = new Ball(startX || pad(WIDTH / 2 + 13), pad(50), ballRadius, 'red', this.ctx, this.obstacles, this.sinks, (index) => {
- this.balls = this.balls.filter(ball => ball !== newBall);
- this.onFinish?.(index, startX)
- });
- this.balls.push(newBall);
- }
+ addBall(startX?: number, multiplier?: number) {
+ const newBall = new Ball(
+ startX || pad(WIDTH / 2 + 13),
+ pad(50),
+ ballRadius,
+ "red",
+ this.ctx,
+ this.obstacles,
+ this.sinks,
+ (index) => {
+ this.balls = this.balls.filter((ball) => ball !== newBall);
+ this.onFinish?.(index, startX, multiplier); // Pass the multiplier to onFinish callback
+ },
+ multiplier
+ ); // Pass the multiplier to the Ball constructor
+ this.balls.push(newBall);
+ }
+ getLastPoints() {
+ return [...this.stack]; // Return a copy of the stack
+ }
- drawObstacles() {
- this.ctx.fillStyle = 'white';
- this.obstacles.forEach((obstacle) => {
- this.ctx.beginPath();
- this.ctx.arc(unpad(obstacle.x), unpad(obstacle.y), obstacle.radius, 0, Math.PI * 2);
- this.ctx.fill();
- this.ctx.closePath();
- });
+ drawObstacles() {
+ this.ctx.fillStyle = "white";
+ this.obstacles.forEach((obstacle) => {
+ this.ctx.beginPath();
+ this.ctx.arc(
+ unpad(obstacle.x),
+ unpad(obstacle.y),
+ obstacle.radius,
+ 0,
+ Math.PI * 2
+ );
+ this.ctx.fill();
+ this.ctx.closePath();
+ });
+ }
+
+ getColor(index: number) {
+ if (index < 3 || index > this.sinks.length - 3) {
+ return { background: "#ff003f", color: "white" };
}
-
- getColor(index: number) {
- if (index <3 || index > this.sinks.length - 3) {
- return {background: '#ff003f', color: 'white'};
- }
- if (index < 6 || index > this.sinks.length - 6) {
- return {background: '#ff7f00', color: 'white'};
- }
- if (index < 9 || index > this.sinks.length - 9) {
- return {background: '#ffbf00', color: 'black'};
- }
- if (index < 12 || index > this.sinks.length - 12) {
- return {background: '#ffff00', color: 'black'};
- }
- if (index < 15 || index > this.sinks.length - 15) {
- return {background: '#bfff00', color: 'black'};
- }
- return {background: '#7fff00', color: 'black'};
+ if (index < 6 || index > this.sinks.length - 6) {
+ return { background: "#ff7f00", color: "white" };
}
- drawSinks() {
- this.ctx.fillStyle = 'green';
- const SPACING = obstacleRadius * 2;
- for (let i = 0; i this.sinks.length - 9) {
+ return { background: "#ffbf00", color: "black" };
}
-
- draw() {
- this.ctx.clearRect(0, 0, WIDTH, HEIGHT);
- this.drawObstacles();
- this.drawSinks();
- this.balls.forEach(ball => {
- ball.draw();
- ball.update();
- });
+ if (index < 12 || index > this.sinks.length - 12) {
+ return { background: "#ffff00", color: "black" };
}
-
- update() {
- this.draw();
- this.requestId = requestAnimationFrame(this.update.bind(this));
+ if (index < 15 || index > this.sinks.length - 15) {
+ return { background: "#bfff00", color: "black" };
}
+ return { background: "#7fff00", color: "black" };
+ }
+ drawSinks() {
+ const SPACING = obstacleRadius * 2;
+ for (let i = 0; i < this.sinks.length; i++) {
+ const sink = this.sinks[i];
+ const color = this.getColor(i).background;
+
+ // Draw rounded rectangle with drop shadow
+ this.ctx.save();
+ this.ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
+ this.ctx.shadowBlur = 5;
+ this.ctx.shadowOffsetX = 2;
+ this.ctx.shadowOffsetY = 2;
+ this.ctx.fillStyle = color;
+ this.roundedRect(
+ sink.x,
+ sink.y - sink.height / 2,
+ sink.width - SPACING + 5,
+ sink.height,
+ 10
+ );
+ this.ctx.fill();
+ this.ctx.restore();
+
+ // Calculate maximum font size to fit in the box
+ const maxFontSize = Math.min(sink.width - SPACING, sink.height) * 0.8;
+
+ // Draw text
+ this.ctx.fillStyle = "white"; // Change text color to white
+ this.ctx.font = `bold ${maxFontSize}px Arial`; // Set font size dynamically
+ this.ctx.textAlign = "center"; // Align text to center
+ this.ctx.textBaseline = "middle"; // Center vertically
+
+ // Truncate text if too long
+ const text = sink?.multiplier?.toString() + "x";
+ const textWidth = this.ctx.measureText(text).width;
+ if (textWidth > sink.width - SPACING) {
+ // Adjust font size to fit in the box
+ this.ctx.font = `bold ${
+ (maxFontSize * (sink.width - SPACING)) / textWidth
+ }px Arial`;
+ }
+
+ this.ctx.fillText(text, sink.x + sinkWidth / 2, sink.y); // Adjust text position
+ }
+ }
+
+ roundedRect(
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ radius: number
+ ) {
+ this.ctx.beginPath();
+ this.ctx.moveTo(x + radius, y);
+ this.ctx.arcTo(x + width, y, x + width, y + height, radius);
+ this.ctx.arcTo(x + width, y + height, x, y + height, radius);
+ this.ctx.arcTo(x, y + height, x, y, radius);
+ this.ctx.arcTo(x, y, x + width, y, radius);
+ this.ctx.closePath();
+ }
+
+ draw() {
+ this.ctx.clearRect(0, 0, WIDTH, HEIGHT);
+ this.drawObstacles();
+ this.drawSinks();
+ this.balls.forEach((ball) => {
+ ball.draw();
+ ball.update();
+ });
+ }
+
+ update() {
+ this.draw();
+ this.requestId = requestAnimationFrame(this.update.bind(this));
+ }
- stop() {
- if (this.requestId) {
- cancelAnimationFrame(this.requestId);
- }
+ stop() {
+ if (this.requestId) {
+ cancelAnimationFrame(this.requestId);
}
-}
\ No newline at end of file
+ }
+}
diff --git a/frontend/src/game/classes/Wave.ts b/frontend/src/game/classes/Wave.ts
new file mode 100644
index 0000000..a0ae087
--- /dev/null
+++ b/frontend/src/game/classes/Wave.ts
@@ -0,0 +1,35 @@
+export class Wave {
+ private x: number;
+ private y: number;
+ private radius: number;
+ private alpha: number;
+ private ctx: CanvasRenderingContext2D;
+
+ constructor(x: number, y: number, ctx: CanvasRenderingContext2D) {
+ this.x = x;
+ this.y = y;
+ this.radius = 0;
+ this.alpha = 1;
+ this.ctx = ctx;
+ }
+
+
+
+ draw() {
+ this.ctx.beginPath();
+ this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
+ this.ctx.strokeStyle = `rgba(255, 255, 255, ${this.alpha})`;
+ this.ctx.lineWidth = 2;
+ this.ctx.stroke();
+ this.ctx.closePath();
+ }
+
+ update() {
+ this.radius += 0.3;
+ this.alpha -= 0.02;
+ }
+
+ isDone() {
+ return this.alpha <= 0;
+ }
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index fcbfe28..456dbad 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -15,8 +15,25 @@
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
- font-family: "Times New Roman", Times, serif;
}
+
+/* Scrollbar Track */
+::-webkit-scrollbar-track {
+ background-color: #f1f1f1; /* Color of the scrollbar track */
+}
+
+/* Scrollbar */
+::-webkit-scrollbar {
+ width: 8px; /* Width of the scrollbar */
+ background-color: #f1f1f1; /* Color of the scrollbar */
+}
+
+/* Scrollbar Thumb */
+::-webkit-scrollbar-thumb {
+ background-color: #888; /* Color of the scrollbar thumb */
+ border-radius: 4px; /* Radius of the scrollbar thumb */
+}
+
/*
a {
font-weight: 500;
diff --git a/frontend/src/pages/Game.tsx b/frontend/src/pages/Game.tsx
index 42f6914..2686c33 100644
--- a/frontend/src/pages/Game.tsx
+++ b/frontend/src/pages/Game.tsx
@@ -3,36 +3,67 @@ import { BallManager } from "../game/classes/BallManager";
import axios from "axios";
import { Button } from "../components/ui";
import { baseURL } from "../utils";
+import "../App.css";
export function Game() {
const [ballManager, setBallManager] = useState();
+ const [lastPoints, setLastPoints] = useState([]);
const canvasRef = useRef();
useEffect(() => {
if (canvasRef.current) {
const ballManager = new BallManager(
- canvasRef.current as unknown as HTMLCanvasElement
+ canvasRef.current as unknown as HTMLCanvasElement,
+ handleBallFinish // Pass the handleBallFinish function to BallManager
);
setBallManager(ballManager);
}
}, [canvasRef]);
+ const handleAddBall = async () => {
+ try {
+ const response = await axios.post(`${baseURL}/game`, {
+ data: 1,
+ });
+ if (ballManager) {
+ const multiplier = response.data.multiplier;
+ ballManager.addBall(response.data.point, multiplier);
+ }
+ } catch (error) {
+ console.error("Error adding ball:", error);
+ }
+ };
+
+ // Callback function to update the stack
+const handleBallFinish = (index: number, startX?: number, multiplier?: number) => {
+ if (multiplier !== undefined) {
+ setLastPoints((prevPoints) => {
+ const updatedPoints = [...prevPoints, multiplier];
+ // If the stack length exceeds 6, remove the oldest entry
+ if (updatedPoints.length > 6) {
+ updatedPoints.shift();
+ }
+ return updatedPoints;
+ });
+ }
+};
+
+
return (
-
);
}