JavaScript · 24250 bytes Raw Blame History
1 // game-systems.js - Core game mechanics and physics
2
3 // ============= CONSTANTS =============
4 // Canvas settings
5 const CANVAS_WIDTH = 800;
6 const CANVAS_HEIGHT = 400;
7
8 // Game constants
9 const BALL_SPEED = 5;
10 const BALL_RADIUS = 12;
11 const PADDLE_WIDTH = 20;
12 const PADDLE_HEIGHT = 80;
13
14 // Enhanced movement constants
15 const SUPPORT_SPEED = 6.5;
16 const SUPPORT_ACCEL = 1.2;
17 const INPUT_SMOOTHING = 0.25;
18 const SUPPORT_MAX_SPEED = 8;
19
20 // Touch/mouse control constants
21 const MOUSE_SPEED_LIMIT = 4;
22 const MOUSE_LAG_FACTOR = 0.12;
23 const TOUCH_SENSITIVITY = 1.2;
24
25 // Spring physics constants
26 const PADDLE_MASS = 0.8;
27 const SPRING_LENGTH = 50;
28 const SPRING_DAMPING = 0.6;
29 const SPRING_STIFFNESS = 0.025;
30
31 // Visual enhancement constants
32 const TRAIL_SEGMENTS = 8;
33 const PADDLE_GLOW_DISTANCE = 25;
34 const SPRING_GLOW_INTENSITY = 120;
35
36 // Particle system constants
37 const MAX_PARTICLES = 100;
38 const PARTICLE_LIFE = 60;
39 const IMPACT_PARTICLES = 8;
40 const SPRING_PARTICLE_RATE = 0.3;
41
42 // Bop system constants
43 const BOP_FORCE = 1.0; // self explanatory. BOP it.
44 const BOP_RANGE = 500; // also self explanatory. TWIST it.
45 const BOP_DURATION = 1000; // traversal duration. SHAKE it.
46 const BOP_COOLDOWN = 0; // also also self expl. PULL it.
47 const ANCHOR_RECOIL = 60; // How far the anchor moves backward during bop
48 const BOP_VELOCITY_BOOST = 5; // Initial velocity boost for paddle
49
50 // Rotation control constants
51 const ROTATION_SPEED = 0.05; // Base rotation speed (radians per frame)
52 const ROTATION_SMOOTHING = 0.15; // Input smoothing for rotation (0-1)
53 const ROTATION_DAMPING = 0.92; // Angular velocity damping (0-1)
54 const ROTATION_MAX_SPEED = 0.2; // Maximum angular velocity (radians per frame)
55 const ROTATION_RESISTANCE = 3.0; // How much harder it is to rotate against momentum
56 const ROTATION_MOMENTUM = 0.85; // How much angular momentum is preserved (0-1)
57 const ROTATION_RETURN_FORCE = 0.02; // Force returning paddle to neutral position
58 const ROTATION_MAX_ANGLE = Math.PI / 4; // Maximum rotation angle (45 degrees)
59 const ROTATION_WITH_MOMENTUM_BOOST = 1.5; // Speed multiplier when rotating with momentum
60 const ROTATION_AGAINST_MOMENTUM_LAG = 0.05; // Lag multiplier when rotating against momentum
61
62 // ============= BOP SYSTEM =============
63 let bopState = {
64 left: {
65 active: false,
66 startTime: 0,
67 duration: BOP_DURATION,
68 power: BOP_FORCE,
69 cooldown: BOP_COOLDOWN,
70 lastBopTime: 0,
71 originalPos: null
72 },
73 right: {
74 active: false,
75 startTime: 0,
76 duration: BOP_DURATION,
77 power: BOP_FORCE,
78 cooldown: BOP_COOLDOWN,
79 lastBopTime: 0,
80 originalPos: null
81 }
82 };
83
84 // ============= ROTATION SYSTEM =============
85 let rotationState = {
86 left: {
87 targetAngle: 0, // Target angle based on input
88 currentAngle: 0, // Current visual angle
89 angularVelocity: 0, // Current angular velocity
90 angularMomentum: 0, // Angular momentum
91 inputBuffer: 0, // Smoothed rotation input (-1 to 1)
92 lastDirection: 0 // Last input direction for momentum checks
93 },
94 right: {
95 targetAngle: 0,
96 currentAngle: 0,
97 angularVelocity: 0,
98 angularMomentum: 0,
99 inputBuffer: 0,
100 lastDirection: 0
101 }
102 };
103
104 function handleBopInput(keys, aiEnabled, currentTime, leftPaddle, rightPaddle, leftSupport, rightSupport, engine, particles) {
105 // Left player bop - use Left Shift for both modes
106 let leftBopPressed = keys['Shift'] && !keys['Control'];
107
108 // BOP_COOLDOWN controls the minimum time between bops
109 if (leftBopPressed && !bopState.left.active &&
110 currentTime - bopState.left.lastBopTime > BOP_COOLDOWN) {
111 activateBop('left', currentTime, leftPaddle, leftSupport, engine, particles);
112 }
113
114 // Right player bop (Enter - only in two player mode)
115 if (!aiEnabled) {
116 let rightBopPressed = keys['Enter'];
117
118 // BOP_COOLDOWN controls the minimum time between bops
119 if (rightBopPressed && !bopState.right.active &&
120 currentTime - bopState.right.lastBopTime > BOP_COOLDOWN) {
121 activateBop('right', currentTime, rightPaddle, rightSupport, engine, particles);
122 }
123 }
124
125 // Update active bops
126 updateBopStates(currentTime, leftSupport, rightSupport, leftPaddle, rightPaddle);
127 }
128
129 function activateBop(side, currentTime, paddle, support, engine, particles) {
130 const Body = Matter.Body;
131
132 bopState[side].active = true;
133 bopState[side].startTime = currentTime;
134 bopState[side].lastBopTime = currentTime;
135
136 // Calculate direction from support to paddle
137 let dx = paddle.position.x - support.position.x;
138 let dy = paddle.position.y - support.position.y;
139
140 // Normalize direction
141 let magnitude = Math.sqrt(dx * dx + dy * dy);
142 if (magnitude > 0) {
143 dx /= magnitude;
144 dy /= magnitude;
145
146 // ANCHOR_RECOIL now properly controls the recoil distance
147 let anchorRecoilDistance = ANCHOR_RECOIL * 0.3; // Can adjust the 0.3 multiplier
148
149 // Move support and paddle backward together
150 Body.translate(support, { x: -dx * anchorRecoilDistance, y: -dy * anchorRecoilDistance });
151 Body.translate(paddle, { x: -dx * anchorRecoilDistance, y: -dy * anchorRecoilDistance });
152
153 // Remember where the support started so we can ease it back later
154 bopState[side].originalPos = {
155 x: support.position.x + dx * anchorRecoilDistance,
156 y: support.position.y + dy * anchorRecoilDistance
157 };
158
159 // BOP_VELOCITY_BOOST controls the initial forward velocity
160 let forwardSpeed = BOP_VELOCITY_BOOST;
161 Body.setVelocity(paddle, {
162 x: paddle.velocity.x + dx * forwardSpeed,
163 y: paddle.velocity.y + dy * forwardSpeed
164 });
165
166 // BOP_FORCE controls the sustained forward thrust
167 // Combined with BOP_RANGE for the total force applied
168 Body.applyForce(paddle, paddle.position, {
169 x: dx * BOP_FORCE * BOP_RANGE * 0.002, // Scaled down for Matter.js
170 y: dy * BOP_FORCE * BOP_RANGE * 0.002
171 });
172
173 // Create particle burst for visual feedback
174 for (let i = 0; i < 5; i++) {
175 let angle = Math.atan2(dy, dx) + (Math.random() - 0.5) * 0.5;
176 let speed = Math.random() * 4 + 2;
177 particles.push({
178 x: support.position.x,
179 y: support.position.y,
180 vx: Math.cos(angle) * speed * -1,
181 vy: Math.sin(angle) * speed * -1,
182 size: Math.random() * 3 + 2,
183 life: 30,
184 maxLife: 30,
185 color: { r: 255, g: 255, b: 100 },
186 type: 'impact'
187 });
188 }
189 }
190
191 console.log(`${side} player BOP! Duration: ${BOP_DURATION}ms, Cooldown: ${BOP_COOLDOWN}ms`);
192 }
193
194 function updateBopStates(currentTime, leftSupport, rightSupport, leftPaddle, rightPaddle) {
195 const Body = Matter.Body;
196
197 // Update left bop
198 if (bopState.left.active) {
199 let elapsed = currentTime - bopState.left.startTime;
200 let progress = elapsed / BOP_DURATION; // BOP_DURATION controls how long the effect lasts
201
202 if (progress >= 1.0) {
203 bopState.left.active = false;
204 bopState.left.originalPos = null;
205 console.log("Left bop ended after", elapsed, "ms");
206 } else {
207 if (bopState.left.originalPos) {
208 let support = leftSupport;
209 let currentX = support.position.x;
210 let currentY = support.position.y;
211
212 // Smooth return motion with easing
213 let returnSpeed = 0.15 * (1 - Math.pow(1 - progress, 3));
214 let newX = currentX + (bopState.left.originalPos.x - currentX) * returnSpeed;
215 let newY = currentY + (bopState.left.originalPos.y - currentY) * returnSpeed;
216
217 Body.setPosition(support, { x: newX, y: newY });
218 }
219
220 limitBopRange(leftSupport, leftPaddle);
221 }
222 }
223
224 // Update right bop (same logic)
225 if (bopState.right.active) {
226 let elapsed = currentTime - bopState.right.startTime;
227 let progress = elapsed / BOP_DURATION; // BOP_DURATION controls how long the effect lasts
228
229 if (progress >= 1.0) {
230 bopState.right.active = false;
231 bopState.right.originalPos = null;
232 console.log("Right bop ended after", elapsed, "ms");
233 } else {
234 if (bopState.right.originalPos) {
235 let support = rightSupport;
236 let currentX = support.position.x;
237 let currentY = support.position.y;
238
239 let returnSpeed = 0.15 * (1 - Math.pow(1 - progress, 3));
240 let newX = currentX + (bopState.right.originalPos.x - currentX) * returnSpeed;
241 let newY = currentY + (bopState.right.originalPos.y - currentY) * returnSpeed;
242
243 Body.setPosition(support, { x: newX, y: newY });
244 }
245
246 limitBopRange(rightSupport, rightPaddle);
247 }
248 }
249 }
250
251 function limitBopRange(support, paddle) {
252 const Body = Matter.Body;
253
254 let currentDistance = dist(support.position.x, support.position.y,
255 paddle.position.x, paddle.position.y);
256
257 // BOP_RANGE controls the maximum extension allowed
258 let maxDistance = SPRING_LENGTH + BOP_RANGE;
259 if (currentDistance > maxDistance) {
260 let dx = paddle.position.x - support.position.x;
261 let dy = paddle.position.y - support.position.y;
262
263 let magnitude = Math.sqrt(dx * dx + dy * dy);
264 dx /= magnitude;
265 dy /= magnitude;
266
267 // Clamp paddle position to max range
268 let newX = support.position.x + dx * maxDistance;
269 let newY = support.position.y + dy * maxDistance;
270
271 let currentVel = paddle.velocity;
272 Body.setPosition(paddle, { x: newX, y: newY });
273
274 // Dampen velocity when hitting the range limit
275 Body.setVelocity(paddle, {
276 x: currentVel.x * 0.7,
277 y: currentVel.y * 0.7
278 });
279 }
280 }
281
282 // ============= PADDLE SYSTEM =============
283 function createSpringPaddleSystem(side, width, height) {
284 const Bodies = Matter.Bodies;
285 const Constraint = Matter.Constraint;
286
287 let supportX = side === 'left' ? 60 : width - 60;
288 let paddleX = side === 'left' ? 60 + SPRING_LENGTH : width - 60 - SPRING_LENGTH;
289 let startY = height / 2;
290
291 let support = Bodies.rectangle(supportX, startY, 10, 10, {
292 isStatic: true,
293 render: { visible: false }
294 });
295
296 let paddleOptions = {
297 mass: PADDLE_MASS,
298 restitution: side === 'left' ? 1.3 : 1.2,
299 friction: 0,
300 frictionAir: side === 'left' ? 0.005 : 0.008,
301 isSensor: false,
302 slop: 0.01,
303 // Add rotational physics
304 inertia: PADDLE_MASS * 200, // Lower inertia = more rotation
305 frictionAngular: 0.02, // Slight angular damping
306 render: {
307 fillStyle: side === 'left' ? '#00ff88' : '#ff6464'
308 }
309 };
310
311 let paddle = Bodies.rectangle(paddleX, startY, PADDLE_WIDTH, PADDLE_HEIGHT, paddleOptions);
312
313 paddle.collisionFilter = {
314 category: side === 'left' ? 0x0002 : 0x0004,
315 mask: 0xFFFF
316 };
317
318 let spring = Constraint.create({
319 bodyA: support,
320 bodyB: paddle,
321 length: SPRING_LENGTH,
322 stiffness: SPRING_STIFFNESS,
323 damping: SPRING_DAMPING,
324 // Add angular stiffness to create torque from movement
325 angularStiffness: 0.01, // Allows paddle to rotate based on spring tension
326 render: { visible: false }
327 });
328
329 return { support, paddle, spring };
330 }
331
332 // ============= MOVEMENT =============
333 function moveSupportEnhanced(support, deltaY, height) {
334 const Body = Matter.Body;
335
336 let newY = support.position.y + deltaY;
337
338 let minY = 50;
339 let maxY = height - 50;
340
341 if (newY < minY) {
342 newY = minY + (newY - minY) * 0.1;
343 } else if (newY > maxY) {
344 newY = maxY + (newY - maxY) * 0.1;
345 }
346
347 Body.setPosition(support, { x: support.position.x, y: newY });
348 }
349
350 // ============= BALL SYSTEM =============
351 function resetBall(ball, world, width, height) {
352 const Bodies = Matter.Bodies;
353 const World = Matter.World;
354 const Body = Matter.Body;
355
356 if (ball) {
357 World.remove(world, ball);
358 }
359
360 ball = Bodies.circle(width/2, height/2, BALL_RADIUS, {
361 restitution: 1,
362 friction: 0,
363 frictionAir: 0,
364 slop: 0.01,
365 collisionFilter: {
366 category: 0x0001,
367 mask: 0xFFFF
368 },
369 render: {
370 fillStyle: '#ff6464'
371 }
372 });
373
374 World.add(world, ball);
375
376 // Start ball moving after a short delay
377 setTimeout(() => {
378 let direction = Math.random() > 0.5 ? 1 : -1;
379 let angle = (Math.random() - 0.5) * Math.PI/3;
380
381 Body.setVelocity(ball, {
382 x: direction * BALL_SPEED * Math.cos(angle),
383 y: BALL_SPEED * Math.sin(angle)
384 });
385 }, 1000);
386
387 return ball;
388 }
389
390 // ============= COLLISION =============
391 function setupCollisionHandlers(engine, ball, leftPaddle, rightPaddle, particles) {
392 const Body = Matter.Body;
393
394 // Track collision state to prevent double-triggering
395 let collisionState = {
396 left: { inCollision: false, lastCollisionTime: 0 },
397 right: { inCollision: false, lastCollisionTime: 0 }
398 };
399
400 Matter.Events.on(engine, 'collisionStart', function(event) {
401 let pairs = event.pairs;
402
403 for (let i = 0; i < pairs.length; i++) {
404 let pair = pairs[i];
405
406 if ((pair.bodyA === ball && (pair.bodyB === leftPaddle || pair.bodyB === rightPaddle)) ||
407 (pair.bodyB === ball && (pair.bodyA === leftPaddle || pair.bodyA === rightPaddle))) {
408
409 let paddle = pair.bodyA === ball ? pair.bodyB : pair.bodyA;
410 let isLeftPaddle = paddle === leftPaddle;
411 let side = isLeftPaddle ? 'left' : 'right';
412
413 // Prevent multiple collisions in quick succession
414 let currentTime = Date.now();
415 if (currentTime - collisionState[side].lastCollisionTime < 100) {
416 continue; // Skip if we just had a collision
417 }
418
419 collisionState[side].lastCollisionTime = currentTime;
420
421 // Apply bop boost if paddle is currently bopping
422 if ((isLeftPaddle && bopState.left.active) || (!isLeftPaddle && bopState.right.active)) {
423 // Get actual collision point from Matter.js
424 let collision = pair.collision;
425 let contactPoint = collision.supports[0];
426
427 // Check if this is a valid collision
428 let isValidCollision = false;
429
430 if (contactPoint) {
431 isValidCollision = true;
432 } else {
433 // Use penetration depth as fallback to avoid false positives
434 const depth = collision.depth || 0;
435 isValidCollision = depth > 0.5;
436 }
437
438 if (isValidCollision) {
439 // During bop, ALWAYS apply boost
440 let ballVel = ball.velocity;
441 let paddleVel = paddle.velocity;
442
443 // Calculate the normal collision response first
444 let normal = collision.normal;
445 let relativeVelocity = {
446 x: ballVel.x - paddleVel.x,
447 y: ballVel.y - paddleVel.y
448 };
449
450 // Calculate the velocity along the collision normal
451 let velocityAlongNormal = relativeVelocity.x * normal.x + relativeVelocity.y * normal.y;
452
453 // Only apply boost if ball is approaching paddle
454 if (velocityAlongNormal < 0) {
455 // Base reflection velocity (enhanced during bop)
456 let restitution = 1.5; // Higher restitution during bop
457 let impulse = 2 * velocityAlongNormal * restitution;
458
459 // Apply the impulse
460 let newVelX = ballVel.x - impulse * normal.x;
461 let newVelY = ballVel.y - impulse * normal.y;
462
463 // Add paddle velocity influence (more during bop)
464 let paddleInfluence = 0.6; // Higher influence during bop
465 newVelX += paddleVel.x * paddleInfluence;
466 newVelY += paddleVel.y * paddleInfluence;
467
468 // Add bop boost in the direction of the normal
469 let bopBoostMagnitude = 3; // Extra boost during bop
470 newVelX += normal.x * bopBoostMagnitude;
471 newVelY += normal.y * bopBoostMagnitude;
472
473 // Ensure minimum speed after bop
474 let newSpeed = Math.sqrt(newVelX * newVelX + newVelY * newVelY);
475 let minSpeed = BALL_SPEED * 1.3; // At least 30% faster than normal
476
477 if (newSpeed < minSpeed) {
478 let scale = minSpeed / newSpeed;
479 newVelX *= scale;
480 newVelY *= scale;
481 }
482
483 // Apply the new velocity
484 Body.setVelocity(ball, {
485 x: newVelX,
486 y: newVelY
487 });
488
489 // Move ball slightly away from paddle to prevent sticking
490 let separation = 2; // pixels
491 Body.setPosition(ball, {
492 x: ball.position.x + normal.x * separation,
493 y: ball.position.y + normal.y * separation
494 });
495
496 // Create impact particles
497 for (let j = 0; j < IMPACT_PARTICLES * 2; j++) { // More particles for bop
498 let angle = Math.atan2(normal.y, normal.x) + (Math.random() - 0.5) * Math.PI;
499 let speed = Math.random() * 8 + 4;
500
501 particles.push({
502 x: contactPoint ? contactPoint.x : ball.position.x,
503 y: contactPoint ? contactPoint.y : ball.position.y,
504 vx: Math.cos(angle) * speed,
505 vy: Math.sin(angle) * speed,
506 size: Math.random() * 5 + 3,
507 life: PARTICLE_LIFE,
508 maxLife: PARTICLE_LIFE,
509 color: { r: 255, g: Math.random() * 155 + 100, b: 50 },
510 type: 'impact'
511 });
512 }
513
514 console.log(`BOP HIT! Clean bounce. New speed: ${newSpeed.toFixed(1)}, Side: ${side}`);
515 }
516 }
517 }
518 }
519 }
520 });
521
522 // Reset collision state on collision end
523 Matter.Events.on(engine, 'collisionEnd', function(event) {
524 let pairs = event.pairs;
525
526 for (let i = 0; i < pairs.length; i++) {
527 let pair = pairs[i];
528
529 if ((pair.bodyA === ball && (pair.bodyB === leftPaddle || pair.bodyB === rightPaddle)) ||
530 (pair.bodyB === ball && (pair.bodyA === leftPaddle || pair.bodyA === rightPaddle))) {
531
532 let paddle = pair.bodyA === ball ? pair.bodyB : pair.bodyA;
533 let isLeftPaddle = paddle === leftPaddle;
534 let side = isLeftPaddle ? 'left' : 'right';
535
536 collisionState[side].inCollision = false;
537 }
538 }
539 });
540 }
541
542 // ============= ROTATION SYSTEM =============
543 function handleRotationInput() {
544 // Left paddle rotation (A/D keys)
545 let leftRotationInput = 0;
546 if (keys['a'] || keys['A']) leftRotationInput -= 1;
547 if (keys['d'] || keys['D']) leftRotationInput += 1;
548
549 // Right paddle rotation (Left/Right arrows, only if AI disabled)
550 let rightRotationInput = 0;
551 if (!aiEnabled) {
552 if (keys['ArrowLeft']) rightRotationInput -= 1;
553 if (keys['ArrowRight']) rightRotationInput += 1;
554 }
555
556 // Update rotation states
557 updateRotationPhysics('left', leftRotationInput, leftPaddle);
558 if (!aiEnabled) {
559 updateRotationPhysics('right', rightRotationInput, rightPaddle);
560 }
561 }
562
563 function updateRotationPhysics(side, input, paddle) {
564 const Body = Matter.Body;
565 let state = rotationState[side];
566
567 // Smooth the input
568 state.inputBuffer = lerp(state.inputBuffer, input, ROTATION_SMOOTHING);
569
570 // Calculate resistance based on momentum
571 let rotatingWithMomentum = (state.inputBuffer * state.angularVelocity) > 0;
572 let effectiveInput = state.inputBuffer;
573
574 if (rotatingWithMomentum && Math.abs(state.inputBuffer) > 0.1) {
575 // Easier to rotate with momentum
576 effectiveInput *= ROTATION_WITH_MOMENTUM_BOOST;
577 } else if (!rotatingWithMomentum && Math.abs(state.inputBuffer) > 0.1) {
578 // Harder to rotate against momentum
579 effectiveInput *= ROTATION_AGAINST_MOMENTUM_LAG;
580 }
581
582 // Apply torque based on input
583 let torque = effectiveInput * ROTATION_SPEED;
584
585 // Update angular velocity with torque
586 state.angularVelocity += torque;
587
588 // Apply damping
589 state.angularVelocity *= ROTATION_DAMPING;
590
591 // Limit maximum angular velocity
592 state.angularVelocity = Math.max(-ROTATION_MAX_SPEED,
593 Math.min(ROTATION_MAX_SPEED, state.angularVelocity));
594
595 // Apply return-to-center force when no input
596 if (Math.abs(state.inputBuffer) < 0.1) {
597 let returnForce = -state.currentAngle * ROTATION_RETURN_FORCE;
598 state.angularVelocity += returnForce;
599 }
600
601 // Update current angle
602 state.currentAngle += state.angularVelocity;
603
604 // Limit maximum rotation angle
605 state.currentAngle = Math.max(-ROTATION_MAX_ANGLE,
606 Math.min(ROTATION_MAX_ANGLE, state.currentAngle));
607
608 // Apply rotation to the paddle body
609 Body.setAngle(paddle, state.currentAngle);
610
611 // Update angular momentum for next frame
612 state.angularMomentum = state.angularMomentum * ROTATION_MOMENTUM +
613 state.angularVelocity * (1 - ROTATION_MOMENTUM);
614
615 // Track last direction for momentum calculations
616 if (Math.abs(input) > 0.1) {
617 state.lastDirection = Math.sign(input);
618 }
619 }
620
621 // ============= HELPER FUNCTIONS =============
622 function lerp(start, stop, amt) {
623 return amt * (stop - start) + start;
624 }