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