Generated idle/serve animation sprites; assist runs at full speed with animation

- new 5-pose sheets per character: idle weight-shift x2, ball-bounce crouch,
  serve trophy pose (toss), overhead smash
- serve shows toss stance through the ball toss, smash frame on contact
- auto-assist: full player speed, adds depth tracking after the bounce,
  and drives the run animation + facing like manual movement

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-07-11 10:22:10 +02:00
1 parent 65bd187633
commit 9268529c8f
17 files changed
+21 -10

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

+21 -10
View File
@@ -18,7 +18,7 @@ const TUNE = {
aimError: 2.4, // m of scatter at quality 0
perfectMs: 45, // |offset from window center| for PERFECT
aiQualityMin: 0.55, aiQualityMax: 0.95, aiFlub: 0.05,
autoAssist: matchMedia('(pointer: coarse)').matches ? 0.65 : 0, // auto-run toward ball x (mobile)
autoAssist: matchMedia('(pointer: coarse)').matches ? 1.0 : 0, // auto-run toward the ball (mobile)
};
const COURT = { L: 23.77, W: 8.23, netZ: 11.885, netH: 0.914, netW: 10.06, serviceD: 6.4 };
@@ -162,8 +162,8 @@ function say(text, delay = 0) { // ponytail: speechSynthesis umpire; swap for re
// ---------------------------------------------------------------- sprites ----
// assets/<name>.png; game falls back to code-drawn figures until sprites load.
const SPR = {};
for (const n of ['p_idle','p_run1','p_run2','p_swing1','p_swing2','p_serve',
'o_idle','o_run1','o_run2','o_swing1','o_swing2','o_serve']) {
for (const n of ['p_idle1','p_idle2','p_bounce','p_toss','p_smash','p_run1','p_run2','p_swing1','p_swing2',
'o_idle1','o_idle2','o_bounce','o_toss','o_smash','o_run1','o_run2','o_swing1','o_swing2']) {
const img = new Image();
img.onload = () => SPR[n] = img;
img.src = 'assets/' + n + '.png';
@@ -396,15 +396,24 @@ function update(dt) {
if (phase === 'rally') {
P.x += mx * TUNE.playerSpeed * slow * dt;
P.z += mz * TUNE.playerSpeed * slow * dt;
let assist = 0;
if (!mx && !mz && TUNE.autoAssist > 0 && ball.active && ball.lastHit === 'O') {
const d = ball.x - P.x; // drift toward the incoming ball so one thumb suffices
P.x += Math.sign(d) * Math.min(Math.abs(d), TUNE.playerSpeed * TUNE.autoAssist * slow * dt);
const rate = TUNE.playerSpeed * TUNE.autoAssist * slow * dt;
const dx = ball.x - P.x; // run toward the incoming ball so one thumb suffices
assist = Math.sign(dx) * Math.min(Math.abs(dx), rate);
P.x += assist;
if (ball.bounces > 0) { // ball on our side: also close the depth gap
const dz = Math.max(COURT.netZ + 1.5, Math.min(24.5, ball.z)) - P.z;
P.z += Math.sign(dz) * Math.min(Math.abs(dz), rate);
if (!assist) assist = dz;
}
}
P.x = Math.max(-6.5, Math.min(6.5, P.x));
P.z = Math.max(COURT.netZ + 1.2, Math.min(26, P.z));
P.runT += (Math.abs(mx) + Math.abs(mz)) * dt;
if (P.pose !== 'swing' && P.pose !== 'serve') P.pose = (mx || mz) ? 'run' : 'idle';
if (mx) P.facing = mx > 0 ? 1 : -1;
const moving = mx || mz || Math.abs(assist) > 0.005;
P.runT += moving ? dt : 0;
if (P.pose !== 'swing' && P.pose !== 'serve') P.pose = moving ? 'run' : 'idle';
if (mx || assist) P.facing = (mx || assist) > 0 ? 1 : -1;
}
}
if (P.swingT >= 0) { P.swingT += dt; trySwing(P, false); if (P.swingT > 0.4) P.swingT = -1; }
@@ -492,11 +501,13 @@ function drawChar(ch) {
const hpx = 1.88 * p.s;
// pick sprite
const pre = ch.side === 'P' ? 'p_' : 'o_';
let name = pre + 'idle';
let name = pre + (Math.floor(idleT * 1.6) % 2 ? 'idle1' : 'idle2'); // idle weight shift
if (phase === 'pretoss' && ch.side === score.server && ch.pose === 'idle')
name = pre + (Math.abs(Math.sin(idleT * 3.6)) < 0.5 ? 'bounce' : 'idle1'); // crouch as the ball drops
if (ch.pose === 'run') name = pre + (Math.floor(ch.runT * 9) % 2 ? 'run1' : 'run2');
if (receiverWaiting) name = pre + (sway > 0 ? 'run1' : 'run2'); // split-step shuffle
if (ch.pose === 'swing') name = pre + (ch.poseT < 0.15 ? 'swing1' : 'swing2');
if (ch.pose === 'serve') name = pre + 'serve';
if (ch.pose === 'serve') name = pre + (phase === 'toss' ? 'toss' : 'smash');
if (ch.pose === 'run') p.y -= Math.floor(ch.runT * 18) % 2; // 1px run bob
const img = SPR[name];
// shadow