JavaScript · 11037 bytes Raw Blame History
1 // Matter.js module aliases
2 const Body = Matter.Body;
3 const World = Matter.World;
4 const Engine = Matter.Engine;
5 const Bodies = Matter.Bodies;
6 const Render = Matter.Render;
7 const Constraint = Matter.Constraint;
8
9 // Game variables
10 let ball;
11 let world;
12 let engine;
13
14 // Spring paddle system components
15 let boundaries = [];
16 let leftSupport, leftPaddle, leftSpring;
17 let rightSupport, rightPaddle, rightSpring;
18
19 // Game state
20 let leftScore = 0;
21 let rightScore = 0;
22 let gameStarted = false;
23
24 // Player input
25 let keys = {};
26
27 // Canvas settings
28 const CANVAS_WIDTH = 800;
29 const CANVAS_HEIGHT = 400;
30
31 // Game constants
32 const BALL_SPEED = 8;
33 const BALL_RADIUS = 12;
34 const PADDLE_WIDTH = 20;
35 const PADDLE_HEIGHT = 80;
36 const SUPPORT_SPEED = 4;
37
38 // Spring physics constants
39 const PADDLE_MASS = 0.8;
40 const SPRING_LENGTH = 40;
41 const SPRING_DAMPING = 0.8;
42 const SPRING_STIFFNESS = 0.02;
43
44 function setup() {
45 // Create p5.js canvas
46 let canvas = createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
47 canvas.parent('gameCanvas');
48
49 // Initialize Matter.js physics engine
50 engine = Engine.create();
51 world = engine.world;
52
53 // Disable gravity for classic Pong feel
54 engine.world.gravity.y = 0;
55 engine.world.gravity.x = 0;
56
57 // Create game boundaries (top and bottom walls)
58 let topWall = Bodies.rectangle(width/2, -10, width, 20, { isStatic: true });
59 let bottomWall = Bodies.rectangle(width/2, height + 10, width, 20, { isStatic: true });
60 boundaries.push(topWall, bottomWall);
61
62 // Create spring paddle systems
63 createSpringPaddleSystem('left');
64 createSpringPaddleSystem('right');
65
66 // Create ball
67 resetBall();
68
69 // Add everything to the world
70 World.add(world, [
71 ...boundaries,
72 ball,
73 leftSupport, leftPaddle, leftSpring,
74 rightSupport, rightPaddle, rightSpring
75 ]);
76 }
77
78 function createSpringPaddleSystem(side) {
79 let supportX = side === 'left' ? 60 : width - 60;
80 let paddleX = side === 'left' ? 60 + SPRING_LENGTH : width - 60 - SPRING_LENGTH;
81 let startY = height / 2;
82
83 if (side === 'left') {
84 // Left support (invisible anchor point controlled by player)
85 leftSupport = Bodies.rectangle(supportX, startY, 10, 10, {
86 isStatic: true,
87 render: { visible: false }
88 });
89
90 // Left paddle (the actual hitting surface)
91 leftPaddle = Bodies.rectangle(paddleX, startY, PADDLE_WIDTH, PADDLE_HEIGHT, {
92 mass: PADDLE_MASS,
93 restitution: 1.2,
94 friction: 0,
95 frictionAir: 0.01
96 });
97
98 // Spring constraint connecting support to paddle
99 leftSpring = Constraint.create({
100 bodyA: leftSupport,
101 bodyB: leftPaddle,
102 length: SPRING_LENGTH,
103 stiffness: SPRING_STIFFNESS,
104 damping: SPRING_DAMPING
105 });
106 } else {
107 // Right support (invisible anchor point controlled by player/AI)
108 rightSupport = Bodies.rectangle(supportX, startY, 10, 10, {
109 isStatic: true,
110 render: { visible: false }
111 });
112
113 // Right paddle (the actual hitting surface)
114 rightPaddle = Bodies.rectangle(paddleX, startY, PADDLE_WIDTH, PADDLE_HEIGHT, {
115 mass: PADDLE_MASS,
116 restitution: 1.2,
117 friction: 0,
118 frictionAir: 0.01
119 });
120
121 // Spring constraint connecting support to paddle
122 rightSpring = Constraint.create({
123 bodyA: rightSupport,
124 bodyB: rightPaddle,
125 length: SPRING_LENGTH,
126 stiffness: SPRING_STIFFNESS,
127 damping: SPRING_DAMPING
128 });
129 }
130 }
131
132 function draw() {
133 // Update physics
134 Engine.update(engine);
135
136 // Handle player input
137 handleInput();
138
139 // Check for scoring
140 checkBallPosition();
141
142 // Clear canvas
143 background(10, 10, 10);
144
145 // Draw game objects
146 drawSpringPaddleSystems();
147 drawBall();
148 drawBoundaries();
149 drawCenterLine();
150
151 // Draw debug info
152 drawDebugInfo();
153
154 // Start message
155 if (!gameStarted) {
156 drawStartMessage();
157 }
158 }
159
160 function drawSpringPaddleSystems() {
161 // Draw springs first (behind paddles)
162 drawSprings();
163
164 // Draw paddles
165 fill(0, 255, 136);
166 stroke(0, 255, 136);
167 strokeWeight(2);
168
169 // Left paddle
170 let leftPos = leftPaddle.position;
171 let leftAngle = leftPaddle.angle;
172 push();
173 translate(leftPos.x, leftPos.y);
174 rotate(leftAngle);
175 rectMode(CENTER);
176 rect(0, 0, PADDLE_WIDTH, PADDLE_HEIGHT);
177 pop();
178
179 // Right paddle
180 let rightPos = rightPaddle.position;
181 let rightAngle = rightPaddle.angle;
182 push();
183 translate(rightPos.x, rightPos.y);
184 rotate(rightAngle);
185 rectMode(CENTER);
186 rect(0, 0, PADDLE_WIDTH, PADDLE_HEIGHT);
187 pop();
188
189 // Draw support points (small indicators)
190 fill(0, 255, 136, 100);
191 noStroke();
192 ellipse(leftSupport.position.x, leftSupport.position.y, 8, 8);
193 ellipse(rightSupport.position.x, rightSupport.position.y, 8, 8);
194 }
195
196 function drawSprings() {
197 stroke(0, 255, 136, 150);
198 strokeWeight(3);
199
200 // Left spring
201 let leftSupportPos = leftSupport.position;
202 let leftPaddlePos = leftPaddle.position;
203
204 // Draw spring as a zigzag line
205 drawSpringLine(leftSupportPos, leftPaddlePos, 'left');
206
207 // Right spring
208 let rightSupportPos = rightSupport.position;
209 let rightPaddlePos = rightPaddle.position;
210
211 drawSpringLine(rightSupportPos, rightPaddlePos, 'right');
212 }
213
214 function drawSpringLine(startPos, endPos, side) {
215 let segments = 8;
216 let amplitude = 8;
217
218 // Calculate spring compression (affects visual amplitude)
219 let currentLength = dist(startPos.x, startPos.y, endPos.x, endPos.y);
220 let compression = SPRING_LENGTH / currentLength;
221 amplitude *= compression;
222
223 stroke(0, 255, 136, 150 + compression * 50); // Brighter when compressed
224 strokeWeight(2 + compression);
225
226 for (let i = 0; i <= segments; i++) {
227 let t = i / segments;
228 let x = lerp(startPos.x, endPos.x, t);
229 let y = lerp(startPos.y, endPos.y, t);
230
231 // Add zigzag offset
232 if (i > 0 && i < segments) {
233 let perpX = -(endPos.y - startPos.y) / currentLength;
234 let perpY = (endPos.x - startPos.x) / currentLength;
235 let offset = sin(i * PI) * amplitude;
236 x += perpX * offset;
237 y += perpY * offset;
238 }
239
240 if (i === 0) {
241 beginShape();
242 vertex(x, y);
243 } else {
244 vertex(x, y);
245 if (i === segments) {
246 endShape();
247 }
248 }
249 }
250 }
251
252 function drawBall() {
253 fill(255, 100, 100);
254 stroke(255, 150, 150);
255 strokeWeight(2);
256
257 let ballPos = ball.position;
258 ellipse(ballPos.x, ballPos.y, BALL_RADIUS * 2, BALL_RADIUS * 2);
259
260 // Ball trail effect
261 fill(255, 100, 100, 50);
262 noStroke();
263 ellipse(ballPos.x, ballPos.y, BALL_RADIUS * 3, BALL_RADIUS * 3);
264 }
265
266 function drawBoundaries() {
267 stroke(0, 255, 136, 30);
268 strokeWeight(1);
269 noFill();
270 line(0, 0, width, 0);
271 line(0, height, width, height);
272 }
273
274 function drawCenterLine() {
275 stroke(0, 255, 136, 50);
276 strokeWeight(2);
277
278 for (let y = 0; y < height; y += 20) {
279 line(width/2, y, width/2, y + 10);
280 }
281 }
282
283 function drawDebugInfo() {
284 fill(255, 100);
285 textAlign(LEFT);
286 textSize(12);
287 text(`FPS: ${Math.round(frameRate())}`, 10, 20);
288 text(`Ball Speed: ${Math.round(getBallSpeed())}`, 10, 35);
289
290 // Spring info
291 let leftSpringLength = dist(leftSupport.position.x, leftSupport.position.y,
292 leftPaddle.position.x, leftPaddle.position.y);
293 let rightSpringLength = dist(rightSupport.position.x, rightSupport.position.y,
294 rightPaddle.position.x, rightPaddle.position.y);
295
296 text(`Left Spring: ${Math.round(leftSpringLength)}px`, 10, 50);
297 text(`Right Spring: ${Math.round(rightSpringLength)}px`, 10, 65);
298 text(`Spring Rest Length: ${SPRING_LENGTH}px`, 10, 80);
299 }
300
301 function drawStartMessage() {
302 fill(0, 255, 136, 200);
303 textAlign(CENTER);
304 textSize(20);
305 text("Press any key to start!", width/2, height/2 + 100);
306 textSize(14);
307 text("Watch the springs compress and extend!", width/2, height/2 + 125);
308 }
309
310 function handleInput() {
311 // Left paddle (W/S keys) - move the support point
312 if (keys['w'] || keys['W']) {
313 moveSupport(leftSupport, -SUPPORT_SPEED);
314 }
315 if (keys['s'] || keys['S']) {
316 moveSupport(leftSupport, SUPPORT_SPEED);
317 }
318
319 // Right paddle (Arrow keys) - move the support point
320 if (keys['ArrowUp']) {
321 moveSupport(rightSupport, -SUPPORT_SPEED);
322 }
323 if (keys['ArrowDown']) {
324 moveSupport(rightSupport, SUPPORT_SPEED);
325 }
326 }
327
328 function moveSupport(support, deltaY) {
329 let newY = support.position.y + deltaY;
330
331 // Keep support within reasonable bounds
332 newY = constrain(newY, 50, height - 50);
333
334 Body.setPosition(support, { x: support.position.x, y: newY });
335 }
336
337 function resetBall() {
338 if (ball) {
339 World.remove(world, ball);
340 }
341
342 // Create new ball at center
343 ball = Bodies.circle(width/2, height/2, BALL_RADIUS, {
344 restitution: 1,
345 friction: 0,
346 frictionAir: 0
347 });
348
349 if (world) {
350 World.add(world, ball);
351 }
352
353 // Start ball moving after a short delay
354 setTimeout(() => {
355 let direction = random() > 0.5 ? 1 : -1;
356 let angle = random(-PI/6, PI/6);
357
358 Body.setVelocity(ball, {
359 x: direction * BALL_SPEED * cos(angle),
360 y: BALL_SPEED * sin(angle)
361 });
362
363 gameStarted = true;
364 }, 1000);
365 }
366
367 function checkBallPosition() {
368 let ballX = ball.position.x;
369
370 if (ballX < -BALL_RADIUS) {
371 rightScore++;
372 updateScore();
373 resetBall();
374 gameStarted = false;
375 }
376
377 if (ballX > width + BALL_RADIUS) {
378 leftScore++;
379 updateScore();
380 resetBall();
381 gameStarted = false;
382 }
383 }
384
385 function updateScore() {
386 document.getElementById('leftScore').textContent = leftScore;
387 document.getElementById('rightScore').textContent = rightScore;
388 }
389
390 function getBallSpeed() {
391 let velocity = ball.velocity;
392 return Math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y);
393 }
394
395 // Input handling
396 function keyPressed() {
397 keys[key] = true;
398 keys[keyCode] = true;
399
400 if (!gameStarted && key !== ' ') {
401 gameStarted = true;
402 }
403
404 // Reset game with spacebar
405 if (key === ' ') {
406 leftScore = 0;
407 rightScore = 0;
408 updateScore();
409 resetBall();
410 gameStarted = false;
411 console.log("🔄 Game reset!");
412 }
413 }
414
415 function keyReleased() {
416 keys[key] = false;
417 keys[keyCode] = false;
418 }