S!0_;>!d
zu=*NC@%O+^nmFtB>OCR-(*GznSpl?S79>!sn)k9T=jK8ot*i%EEm$<9gyQPC9y#nk
z0@@3hTph@~r~A7>?GX`p_1`=jjM|4{FzY{(ZMAP?;9&PnzC&$Z3jB4)CXlSXUk)n}
z|KE96tR%5WEQ6A${B%L3af30MZidj9Bn)lCqazG6-0K35XyLq^`i$7^&p@>DaTy?wph2!Q;D#WFFt|$)&FVV|44^^w;cJ_wIDUi#R56G$2Hb&w
z7X}sM??T`p7M22o7=(<;x(|UaZkPm=SWr|{iIP|lTTE{?1aSOlcwi%J`Vaz~tZ4W|
zEd)GS5i3j+I;mtuoG^Ly5a41%4)gduhM=H8pAg?@k6myIsXI#
zWOmeL7@dfS!$k0Y3xWrA><9s~y%7RK?5LkP9Rg`YSP^(}poz8HAP^-3YXZgwQf!AV
zZ9yC{GTjhx*@8Cb6B7b;TToRY1cUePsA>-u<;}6fK*bMM00kSUnHL7@M`2m8j3Oim
z4-`2O7lc(5q;et@3{3(CJ#i?Pj|=5qkb%L=WLOM%ZlD1a38ug@pbkYyqb!)=LOc;u
z6JW)SR&m`71|y}gG+=O}T)Z_54wl0zK!pb(b75^@$S68I!oxh!2V}D&TfrrM1PA`$
zK?sPw4S2wV972>F0T(agiZES)7e68hsJzGljG8+P!Wz(&bG)eOfe;v^GT`mN0*4}x
z(7#jQK}8oV0jf4aNEi(EyoBWdFCSW=O7y0aBOhXiNjL?A#zA!3edrPQQ6H*7>s^KO|IgREds1;8$Z1j
OIzq%C$om|OqW=TAny(=M
diff --git a/thiola-pong/constants.js b/thiola-pong/constants.js
index 84564d5..6826d1a 100644
--- a/thiola-pong/constants.js
+++ b/thiola-pong/constants.js
@@ -2,11 +2,14 @@ const PADDLE_W = 12;
const PADDLE_H = 90;
const PADDLE_MARGIN = 42;
const BALL_R = 7;
-const BASE_SPEED = 5;
+const BASE_SPEED = 3.5;
const WIN_SCORE = 5;
const MAX_LEVEL = 3;
const WALL_WIDTH = 32;
+const PADDLE_SPIN_FACTOR = 0.5;
+const BALL_RESUME_DELAY = 800; // ms pause after dismissing a goal message
+
const LEVEL_COVERAGE = [0, 0, 40, 70];
const LEVEL_CPU_SPEED = [0, 1.6, 3.0, 3.8];
diff --git a/thiola-pong/game.js b/thiola-pong/game.js
index 03eb558..cddb6ac 100644
--- a/thiola-pong/game.js
+++ b/thiola-pong/game.js
@@ -116,7 +116,7 @@
goalFlash.removeEventListener('click', cont);
if (playerScore >= WIN_SCORE) { advanceLevel(); }
else if (cpuScore >= WIN_SCORE) { showGameOver(); }
- else { paused = false; resetBall(isPlayerGoal ? -1 : 1); }
+ else { resetBall(isPlayerGoal ? -1 : 1); setTimeout(() => { paused = false; }, BALL_RESUME_DELAY); }
}
setTimeout(() => {
@@ -247,17 +247,19 @@
if (e.key === 'w' || e.key === 'W') keyW = false;
if (e.key === 's' || e.key === 'S') keyS = false;
});
- canvas.addEventListener('mousemove', e => {
+ document.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
mouseY = (e.clientY - rect.top) * (H / rect.height);
});
- canvas.addEventListener('mouseleave', () => { mouseY = null; });
// ─── Update ───
function update() {
if (!running || paused) return;
glowPhase += 0.03;
+ const prevPlayerY = playerY;
+ const prevCpuY = cpuY;
+
// Player 1 (left) — PvP: W/S only; CPU mode: mouse OR W/S
if (gameMode === 'pvp') {
if (keyW) playerY -= 6;
@@ -282,6 +284,9 @@
}
cpuY = Math.max(0, Math.min(H - PADDLE_H, cpuY));
+ const playerVY = playerY - prevPlayerY;
+ const cpuVY = cpuY - prevCpuY;
+
// Ball movement
ballX += ballVX; ballY += ballVY;
if (ballY - BALL_R <= 0) { ballY = BALL_R; ballVY = Math.abs(ballVY); }
@@ -292,7 +297,7 @@
if (ballX + BALL_R >= W - PADDLE_MARGIN - PADDLE_W) {
if (ballX + BALL_R <= W - PADDLE_MARGIN + PADDLE_W && ballY >= cpuY && ballY <= cpuY + PADDLE_H) {
ballVX = -Math.abs(ballVX) * 1.03;
- ballVY += ((ballY - cpuY)/PADDLE_H - 0.5) * 3;
+ ballVY += ((ballY - cpuY)/PADDLE_H - 0.5) * 3 + cpuVY * PADDLE_SPIN_FACTOR;
ballX = W - PADDLE_MARGIN - PADDLE_W - BALL_R;
spawnParticles(ballX, ballY, '#ff6b35', 8);
} else if (ballX > W + BALL_R) {
@@ -305,7 +310,7 @@
if (ballX - BALL_R <= PADDLE_MARGIN + PADDLE_W) {
if (ballX - BALL_R >= PADDLE_MARGIN - PADDLE_W && ballY >= playerY && ballY <= playerY + PADDLE_H) {
ballVX = Math.abs(ballVX) * 1.03;
- ballVY += ((ballY - playerY)/PADDLE_H - 0.5) * 3;
+ ballVY += ((ballY - playerY)/PADDLE_H - 0.5) * 3 + playerVY * PADDLE_SPIN_FACTOR;
ballX = PADDLE_MARGIN + PADDLE_W + BALL_R;
spawnParticles(ballX, ballY, '#4ecdc4', 8);
} else if (ballX - BALL_R <= WALL_WIDTH && coveragePercent > 0) {