diff --git a/assets/_sheet_awalk.png b/assets/_sheet_awalk.png new file mode 100644 index 0000000..bea7bbb Binary files /dev/null and b/assets/_sheet_awalk.png differ diff --git a/assets/_sheet_bwalk.png b/assets/_sheet_bwalk.png new file mode 100644 index 0000000..97d027a Binary files /dev/null and b/assets/_sheet_bwalk.png differ diff --git a/assets/_sheet_fsprint.png b/assets/_sheet_fsprint.png new file mode 100644 index 0000000..1ac09ca Binary files /dev/null and b/assets/_sheet_fsprint.png differ diff --git a/assets/z_a1.png b/assets/z_a1.png index 75de561..89f45e4 100644 Binary files a/assets/z_a1.png and b/assets/z_a1.png differ diff --git a/assets/z_a2.png b/assets/z_a2.png index 2ddb352..6f6bc59 100644 Binary files a/assets/z_a2.png and b/assets/z_a2.png differ diff --git a/assets/z_a3.png b/assets/z_a3.png new file mode 100644 index 0000000..6f9d225 Binary files /dev/null and b/assets/z_a3.png differ diff --git a/assets/z_a4.png b/assets/z_a4.png new file mode 100644 index 0000000..71bdea4 Binary files /dev/null and b/assets/z_a4.png differ diff --git a/assets/z_b1.png b/assets/z_b1.png index 1258f31..def6fdf 100644 Binary files a/assets/z_b1.png and b/assets/z_b1.png differ diff --git a/assets/z_b2.png b/assets/z_b2.png index 74ddc7c..8acf96c 100644 Binary files a/assets/z_b2.png and b/assets/z_b2.png differ diff --git a/assets/z_b3.png b/assets/z_b3.png new file mode 100644 index 0000000..4ef90f7 Binary files /dev/null and b/assets/z_b3.png differ diff --git a/assets/z_b4.png b/assets/z_b4.png new file mode 100644 index 0000000..e6df9b5 Binary files /dev/null and b/assets/z_b4.png differ diff --git a/assets/z_f1.png b/assets/z_f1.png index 164cce5..6a2dacb 100644 Binary files a/assets/z_f1.png and b/assets/z_f1.png differ diff --git a/assets/z_f2.png b/assets/z_f2.png index 0e63511..660b2dc 100644 Binary files a/assets/z_f2.png and b/assets/z_f2.png differ diff --git a/assets/z_f3.png b/assets/z_f3.png new file mode 100644 index 0000000..6c68d21 Binary files /dev/null and b/assets/z_f3.png differ diff --git a/assets/z_f4.png b/assets/z_f4.png new file mode 100644 index 0000000..e5ad955 Binary files /dev/null and b/assets/z_f4.png differ diff --git a/config.js b/config.js index bbd391e..840a67b 100644 --- a/config.js +++ b/config.js @@ -2,7 +2,7 @@ // the exported consts below are structural values that code shares but the panel doesn't show. export const TUNE = { - camZ: 36, camY: 9, camF: 280, horizon: 40, + camZ: 42, camY: 9, camF: 406, horizon: 40, // camF/horizon are per-orientation, set in fitViewport gravity: 9.2, // slightly floaty vs 9.8 for readability spinFactor: 0.9, // extra dive on quality shots (fake topspin) bounceY: 0.58, bounceXZ: 0.78, diff --git a/main.js b/main.js index a7281ab..0a7dc2d 100644 --- a/main.js +++ b/main.js @@ -6,7 +6,7 @@ import { TUNE, COURT, UMP_H } from './config.js'; import { P, O, ball, score, G, setupPoint } from './state.js'; import { update, keyPressed, solveShot, inServeBox, inCourt, forceZombies, splatPoint } from './gameplay.js'; -import { fitViewport, render, proj } from './render.js'; +import { fitViewport, render, proj, RES } from './render.js'; import { voice } from './audio.js'; import { zombies, spawnZombie, piles, gore, parts, pellets, extras, explode } from './zombies.js'; import { cv } from './dom.js'; @@ -40,8 +40,8 @@ addEventListener('keydown', maybeRestart); let umpTaps = []; cv.addEventListener('pointerdown', e => { const r = cv.getBoundingClientRect(); - const x = (e.clientX - r.left) / r.width * (cv.width / 2); // logical coords (RES = 2) - const y = (e.clientY - r.top) / r.height * (cv.height / 2); + const x = (e.clientX - r.left) / r.width * (cv.width / RES); // logical coords + const y = (e.clientY - r.top) / r.height * (cv.height / RES); if (x >= 6 && x <= 138 && y >= 6 && y <= 36) { G.logShow = !G.logShow; return; } // scoreboard tap = miss-log overlay const u = proj(7.1, 0, COURT.netZ); if (Math.abs(x - u.x) > 16 || y > u.y + 6 || y < u.y - UMP_H * u.s - 8) return; diff --git a/render.js b/render.js index e990e50..f05679d 100644 --- a/render.js +++ b/render.js @@ -30,7 +30,7 @@ function palette() { } // ---------------------------------------------------------------- camera ---- -export const RES = 2; // physical pixels per logical pixel — sprites keep 2x detail +export const RES = 3; // physical pixels per logical pixel — 3x so phone screens downscale (crisp) instead of upscaling (soft) export let W = 480, H = 270; export function fitViewport() { // landscape 480x270, portrait 270x480 with a longer lens @@ -39,7 +39,9 @@ export function fitViewport() { // landscape 480x270, portrait 270x480 with a lo if (cv.width !== w * RES) { W = w; H = h; cv.width = w * RES; cv.height = h * RES; - TUNE.camF = portrait ? 400 : 280; + // flatter lens: camZ pushed back (config 42, was 36) with camF scaled to keep the PLAYER + // the same size — far-court zombies render ~25% bigger and actually read as zombies + TUNE.camF = portrait ? 580 : 406; TUNE.horizon = portrait ? 90 : 40; buildCrowd(); } @@ -60,8 +62,8 @@ const SPR = {}; for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','p_run1','p_run2','p_swing1','p_swing2', 'o_idle1','o_idle2','o_bounce','o_bounce2','o_toss','o_smash','o_run1','o_run2','o_swing1','o_swing2', 'x_ump','x_boyrun1','x_boyrun2','x_boyrun3','x_boyrun4', - 'z_a1','z_a2','z_b1','z_b2','z_c1','z_c2', - 'z_f1','z_f2','z_g1','z_g2','z_s1','z_s2','z_s3']) { + 'z_a1','z_a2','z_a3','z_a4','z_b1','z_b2','z_b3','z_b4','z_c1','z_c2', + 'z_f1','z_f2','z_f3','z_f4','z_g1','z_g2','z_s1','z_s2','z_s3']) { const img = new Image(); img.onload = () => SPR[n] = img; img.src = 'assets/' + n + '.png'; @@ -129,10 +131,14 @@ function drawBoy(b) { ctx.drawImage(img, -w / 2, -h, w, h); ctx.restore(); } +const ZFRAMES = { a: 4, b: 4, f: 4 }; // 4-frame walk cycles where sheets exist; the rest stay 2 function drawZombie(zb) { const rk = zb.variant === 'r'; const v = rk ? 'c' : zb.variant; // the racket boss reuses the turned-CPU sprite (already holds a racket) - let frame = rk && zb.pose === 'swing' ? 2 : Math.floor(zb.walkT * (zb.variant === 'f' ? 6 : 2.6)) % 2 + 1; + let N = ZFRAMES[v] || 2; + if (!SPR['z_' + v + N]) N = 2; // frames still loading (or sheet rejected): 2-frame fallback + // same CYCLE frequency as before — more frames subdivide the stride, they don't speed it up + let frame = rk && zb.pose === 'swing' ? 2 : Math.floor(zb.walkT * (zb.variant === 'f' ? 6 : 2.6) * N / 2) % N + 1; if (zb.variant === 's' && zb.shieldT > 0) frame = 3; // chair dropped — hit it NOW const h = zb.variant === 'g' ? 2.7 : rk ? 1.85 : 1.8; // the brute towers half again over the crowd drawStatic('z_' + v + frame, zb.x, zb.z, h, Math.floor(zb.walkT * 5.2) % 2); diff --git a/zombies.js b/zombies.js index 851517c..2202bab 100644 --- a/zombies.js +++ b/zombies.js @@ -39,18 +39,25 @@ function pickVariant() { // core kill: remove the zombie, spawn its gore/pile/particles, score. No ball coupling — // shared by ball hits, machine-gun pellets, melee swipes, and extra (multi-serve) balls. +// how absurdly a variant dies: classics splash, the advanced roster DETONATES +const GORE_MUL = { a: 1, b: 1, f: 1.8, s: 2.5, g: 4, r: 3 }; + function killZombie(i, quiet) { const zb = zombies[i]; - const big = zb.variant === 'g'; // a brute goes down like a building — twice the gore + const mul = GORE_MUL[zb.variant] || 1; zombies.splice(i, 1); if (G.interlude && zb.variant === 'r') resolveInterlude('P'); // regicide ends the duel your way — any weapon counts - gore.push({ x: zb.x, z: zb.z, r: (big ? 0.8 : 0.45) + Math.random() * 0.35, a: 0.85 }); - if (gore.length > 40) gore.shift(); + const stains = Math.round(Math.sqrt(mul)); // advanced kills splash a scatter of stains, not one puddle + for (let k = 0; k < stains; k++) { + gore.push({ x: zb.x + (k ? (Math.random() * 2 - 1) * 0.9 : 0), z: zb.z + (k ? (Math.random() * 2 - 1) * 0.9 : 0), + r: (0.45 + Math.random() * 0.35) * Math.sqrt(mul), a: 0.85 }); + if (gore.length > 40) gore.shift(); + } const pl = piles.find(p => Math.hypot(p.x - zb.x, p.z - zb.z) < 0.6); // stack up where kills cluster - if (pl) pl.size = Math.min(4, pl.size + (big ? 2 : 1)); - else { piles.push({ x: zb.x, z: zb.z, size: big ? 2 : 1, seed: Math.random() * 1e4, blobs: null, builtSize: 0 }); + if (pl) pl.size = Math.min(4, pl.size + Math.round(mul / 2 + 0.5)); + else { piles.push({ x: zb.x, z: zb.z, size: Math.min(4, Math.round(mul / 2 + 0.5)), seed: Math.random() * 1e4, blobs: null, builtSize: 0 }); if (piles.length > 30) piles.shift(); } - const n = big ? 48 : 24; + const n = Math.round(30 * mul); // base bumped 24 -> 30, then the multiplier goes to work for (let k = 0; k < n; k++) { const dark = k >= n * 2 / 3; parts.push({ // ~1/3 dark & long-lived: settling gore x: zb.x, y: 0.9, z: zb.z, vx: (Math.random() * 2 - 1) * 3.5, vy: 1 + Math.random() * 3.5, vz: (Math.random() * 2 - 1) * 3.5,