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 (
- +
+
    + {lastPoints.map((point, index) => ( +
  • + {point}x +
  • + ))} +
+
); }