JavaScript · 13459 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 = 6;
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 = 300; // 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 = 12; // 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 const Engine = Matter.Engine;
99
100 bopState[side].active = true;
101 bopState[side].startTime = currentTime;
102 bopState[side].lastBopTime = currentTime;
103
104 // Calculate direction from support to paddle
105 let dx = paddle.position.x - support.position.x;
106 let dy = paddle.position.y - support.position.y;
107
108 // Normalize direction
109 let magnitude = Math.sqrt(dx * dx + dy * dy);
110 if (magnitude > 0) {
111 dx /= magnitude;
112 dy /= magnitude;
113
114 // Calculate anchor recoil distance
115 let anchorRecoilDistance = ANCHOR_RECOIL * 0.4;
116
117 // Move the support BACKWARD (recoil effect)
118 let newSupportX = support.position.x - dx * anchorRecoilDistance;
119 let newSupportY = support.position.y - dy * anchorRecoilDistance;
120
121 Body.setPosition(support, { x: newSupportX, y: newSupportY });
122
123 // Store original support position for recovery
124 bopState[side].originalPos = {
125 x: support.position.x + dx * anchorRecoilDistance,
126 y: support.position.y + dy * anchorRecoilDistance
127 };
128
129 // Set paddle velocity directly for immediate forward thrust
130 let forwardSpeed = (BOP_RANGE / SPRING_LENGTH) * BOP_VELOCITY_BOOST;
131 Body.setVelocity(paddle, {
132 x: paddle.velocity.x + dx * forwardSpeed,
133 y: paddle.velocity.y + dy * forwardSpeed
134 });
135
136 // Apply a strong forward force for continued acceleration
137 Body.applyForce(paddle, paddle.position, {
138 x: dx * bopState[side].power * BOP_RANGE * 0.1,
139 y: dy * bopState[side].power * BOP_RANGE * 0.1
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 // Force collision detection update
160 Engine.update(engine, 0);
161 }
162
163 console.log(side + " player BOP!");
164 }
165
166 function updateBopStates(currentTime, leftSupport, rightSupport, leftPaddle, rightPaddle) {
167 const Body = Matter.Body;
168
169 // Update left bop
170 if (bopState.left.active) {
171 let elapsed = currentTime - bopState.left.startTime;
172 let progress = elapsed / bopState.left.duration;
173
174 if (progress >= 1.0) {
175 bopState.left.active = false;
176 bopState.left.originalPos = null;
177 } else {
178 if (bopState.left.originalPos) {
179 let support = leftSupport;
180 let currentX = support.position.x;
181 let currentY = support.position.y;
182
183 let returnSpeed = 0.15 * (1 - Math.pow(1 - progress, 3));
184 let newX = currentX + (bopState.left.originalPos.x - currentX) * returnSpeed;
185 let newY = currentY + (bopState.left.originalPos.y - currentY) * returnSpeed;
186
187 Body.setPosition(support, { x: newX, y: newY });
188 }
189
190 limitBopRange(leftSupport, leftPaddle);
191 }
192 }
193
194 // Update right bop
195 if (bopState.right.active) {
196 let elapsed = currentTime - bopState.right.startTime;
197 let progress = elapsed / bopState.right.duration;
198
199 if (progress >= 1.0) {
200 bopState.right.active = false;
201 bopState.right.originalPos = null;
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 let maxDistance = SPRING_LENGTH + BOP_RANGE;
227 if (currentDistance > maxDistance) {
228 let dx = paddle.position.x - support.position.x;
229 let dy = paddle.position.y - support.position.y;
230
231 let magnitude = Math.sqrt(dx * dx + dy * dy);
232 dx /= magnitude;
233 dy /= magnitude;
234
235 let newX = support.position.x + dx * maxDistance;
236 let newY = support.position.y + dy * maxDistance;
237
238 let currentVel = paddle.velocity;
239 Body.setPosition(paddle, { x: newX, y: newY });
240 Body.setVelocity(paddle, {
241 x: currentVel.x * 0.7,
242 y: currentVel.y * 0.7
243 });
244 }
245 }
246
247 // ============= PADDLE SYSTEM =============
248 function createSpringPaddleSystem(side, width, height) {
249 const Bodies = Matter.Bodies;
250 const Constraint = Matter.Constraint;
251
252 let supportX = side === 'left' ? 60 : width - 60;
253 let paddleX = side === 'left' ? 60 + SPRING_LENGTH : width - 60 - SPRING_LENGTH;
254 let startY = height / 2;
255
256 let support = Bodies.rectangle(supportX, startY, 10, 10, {
257 isStatic: true,
258 render: { visible: false }
259 });
260
261 let paddleOptions = {
262 mass: PADDLE_MASS,
263 restitution: side === 'left' ? 1.3 : 1.2,
264 friction: 0,
265 frictionAir: side === 'left' ? 0.005 : 0.008,
266 isSensor: false,
267 slop: 0.01,
268 render: {
269 fillStyle: side === 'left' ? '#00ff88' : '#ff6464'
270 }
271 };
272
273 let paddle = Bodies.rectangle(paddleX, startY, PADDLE_WIDTH, PADDLE_HEIGHT, paddleOptions);
274
275 paddle.collisionFilter = {
276 category: side === 'left' ? 0x0002 : 0x0004,
277 mask: 0xFFFF
278 };
279
280 let spring = Constraint.create({
281 bodyA: support,
282 bodyB: paddle,
283 length: SPRING_LENGTH,
284 stiffness: SPRING_STIFFNESS,
285 damping: SPRING_DAMPING
286 });
287
288 return { support, paddle, spring };
289 }
290
291 // ============= MOVEMENT =============
292 function moveSupportEnhanced(support, deltaY, height) {
293 const Body = Matter.Body;
294
295 let newY = support.position.y + deltaY;
296
297 let minY = 50;
298 let maxY = height - 50;
299
300 if (newY < minY) {
301 newY = minY + (newY - minY) * 0.1;
302 } else if (newY > maxY) {
303 newY = maxY + (newY - maxY) * 0.1;
304 }
305
306 Body.setPosition(support, { x: support.position.x, y: newY });
307 }
308
309 // ============= BALL SYSTEM =============
310 function resetBall(ball, world, width, height) {
311 const Bodies = Matter.Bodies;
312 const World = Matter.World;
313 const Body = Matter.Body;
314
315 if (ball) {
316 World.remove(world, ball);
317 }
318
319 ball = Bodies.circle(width/2, height/2, BALL_RADIUS, {
320 restitution: 1,
321 friction: 0,
322 frictionAir: 0,
323 slop: 0.01,
324 collisionFilter: {
325 category: 0x0001,
326 mask: 0xFFFF
327 },
328 render: {
329 fillStyle: '#ff6464'
330 }
331 });
332
333 World.add(world, ball);
334
335 // Start ball moving after a short delay
336 setTimeout(() => {
337 let direction = Math.random() > 0.5 ? 1 : -1;
338 let angle = (Math.random() - 0.5) * Math.PI/3;
339
340 Body.setVelocity(ball, {
341 x: direction * BALL_SPEED * Math.cos(angle),
342 y: BALL_SPEED * Math.sin(angle)
343 });
344 }, 1000);
345
346 return ball;
347 }
348
349 // ============= COLLISION =============
350 function setupCollisionHandlers(engine, ball, leftPaddle, rightPaddle, particles) {
351 const Body = Matter.Body;
352
353 Matter.Events.on(engine, 'collisionStart', function(event) {
354 let pairs = event.pairs;
355
356 for (let i = 0; i < pairs.length; i++) {
357 let pair = pairs[i];
358
359 if ((pair.bodyA === ball && (pair.bodyB === leftPaddle || pair.bodyB === rightPaddle)) ||
360 (pair.bodyB === ball && (pair.bodyA === leftPaddle || pair.bodyA === rightPaddle))) {
361
362 let paddle = pair.bodyA === ball ? pair.bodyB : pair.bodyA;
363 let isLeftPaddle = paddle === leftPaddle;
364
365 if ((isLeftPaddle && bopState.left.active) || (!isLeftPaddle && bopState.right.active)) {
366 let ballVel = ball.velocity;
367 let paddleVel = paddle.velocity;
368
369 let boostX = paddleVel.x * 0.5;
370 let boostY = paddleVel.y * 0.5;
371
372 Body.setVelocity(ball, {
373 x: ballVel.x * 1.3 + boostX,
374 y: ballVel.y * 1.3 + boostY
375 });
376
377 // Create impact particles
378 for (let j = 0; j < IMPACT_PARTICLES; j++) {
379 let angle = Math.random() * Math.PI * 2;
380 let speed = Math.random() * 6 + 2;
381
382 particles.push({
383 x: ball.position.x,
384 y: ball.position.y,
385 vx: Math.cos(angle) * speed - ballVel.x * 0.2,
386 vy: Math.sin(angle) * speed - ballVel.y * 0.2,
387 size: Math.random() * 4 + 2,
388 life: PARTICLE_LIFE,
389 maxLife: PARTICLE_LIFE,
390 color: { r: 255, g: Math.random() * 155 + 100, b: Math.random() * 50 + 100 },
391 type: 'impact'
392 });
393 }
394 }
395 }
396 }
397 });
398 }