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