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>
No files matched your search
|
After Width: | Height: | Size: 626 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
@@ -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
|
||||
|
||||