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