diff --git a/assets/_zsheet.py b/assets/_zsheet.py index 0a4cff7..d8ead2d 100644 --- a/assets/_zsheet.py +++ b/assets/_zsheet.py @@ -96,11 +96,28 @@ def cut(sheet, fam, outdir): px = big[big[..., 3] > 200][:, :3] pal = np.unique((px // 8) * 8, axis=0) + # A FALL ROW IS ANCHORED ON ITS OWN BUCKLE, not on the sheet. The generator draws the fall rows + # noticeably smaller than the standing rows — the brute's buckle came back at 190 px against a + # 224 px stance — and one global scale preserves that faithfully, so the body SHRANK the moment + # it started to fall ("the fall animation seems shrunken"). The first frame of a fall is a body + # still on its feet, so it must draw at standing height; the other three are scaled by the same + # factor, which keeps the collapse's own proportions intact. A row that was already right (the + # racket family's, at 224/224) gets a factor of 1.0 and is untouched — which is the check that + # this rule is the right one rather than a fudge for one bad sheet. + mult = [1.0] * len(crops) + i = 0 + for row in want: + if row[0].endswith('fall1') and crops[i].shape[0]: + k_row = crops[0].shape[0] / crops[i].shape[0] + for j in range(i, i + len(row)): + mult[j] = k_row + i += len(row) + os.makedirs(outdir, exist_ok=True) - for c, name in zip(crops, flat): + for c, name, m in zip(crops, flat, mult): h, w = c.shape[:2] snap = Image.fromarray(c, 'RGBA').resize((max(1, round(w / b)), max(1, round(h / b))), Image.BOX) - im = snap.resize((max(1, round(snap.width * k)), max(1, round(snap.height * k))), Image.BOX) + im = snap.resize((max(1, round(snap.width * k * m)), max(1, round(snap.height * k * m))), Image.BOX) arr = np.array(im).astype(int) solid = arr[..., 3] > 110 f = arr[..., :3].reshape(-1, 3) diff --git a/assets/z_bbfall1.png b/assets/z_bbfall1.png index f5efda3..6aa9911 100644 Binary files a/assets/z_bbfall1.png and b/assets/z_bbfall1.png differ diff --git a/assets/z_bbfall2.png b/assets/z_bbfall2.png index fda1630..f4d3c29 100644 Binary files a/assets/z_bbfall2.png and b/assets/z_bbfall2.png differ diff --git a/assets/z_bbfall3.png b/assets/z_bbfall3.png index 8520012..52fb8c6 100644 Binary files a/assets/z_bbfall3.png and b/assets/z_bbfall3.png differ diff --git a/assets/z_bbrise.png b/assets/z_bbrise.png index fc7a23d..97a9c06 100644 Binary files a/assets/z_bbrise.png and b/assets/z_bbrise.png differ diff --git a/assets/z_bbw1fall1.png b/assets/z_bbw1fall1.png index d89cca6..e284221 100644 Binary files a/assets/z_bbw1fall1.png and b/assets/z_bbw1fall1.png differ diff --git a/assets/z_bbw1fall2.png b/assets/z_bbw1fall2.png index ba788c0..9c78b66 100644 Binary files a/assets/z_bbw1fall2.png and b/assets/z_bbw1fall2.png differ diff --git a/assets/z_bbw1fall3.png b/assets/z_bbw1fall3.png index 6bc1b16..5a612ef 100644 Binary files a/assets/z_bbw1fall3.png and b/assets/z_bbw1fall3.png differ diff --git a/assets/z_bbw1rise.png b/assets/z_bbw1rise.png index b99236c..58ac53d 100644 Binary files a/assets/z_bbw1rise.png and b/assets/z_bbw1rise.png differ diff --git a/assets/z_bbw2fall1.png b/assets/z_bbw2fall1.png index 9f08f96..bd79bc7 100644 Binary files a/assets/z_bbw2fall1.png and b/assets/z_bbw2fall1.png differ diff --git a/assets/z_bbw2fall2.png b/assets/z_bbw2fall2.png index 24274eb..e879c25 100644 Binary files a/assets/z_bbw2fall2.png and b/assets/z_bbw2fall2.png differ diff --git a/assets/z_bbw2fall3.png b/assets/z_bbw2fall3.png index 4d6dfa9..6a10871 100644 Binary files a/assets/z_bbw2fall3.png and b/assets/z_bbw2fall3.png differ diff --git a/assets/z_bbw2rise.png b/assets/z_bbw2rise.png index 03e324d..58e46f3 100644 Binary files a/assets/z_bbw2rise.png and b/assets/z_bbw2rise.png differ diff --git a/assets/z_brfall1.png b/assets/z_brfall1.png index 73f1429..3a383a3 100644 Binary files a/assets/z_brfall1.png and b/assets/z_brfall1.png differ diff --git a/assets/z_brfall2.png b/assets/z_brfall2.png index 0e4633d..99052c3 100644 Binary files a/assets/z_brfall2.png and b/assets/z_brfall2.png differ diff --git a/assets/z_brfall3.png b/assets/z_brfall3.png index 2b53db8..96be3b7 100644 Binary files a/assets/z_brfall3.png and b/assets/z_brfall3.png differ diff --git a/assets/z_brrise.png b/assets/z_brrise.png index daab3fc..ef37a4e 100644 Binary files a/assets/z_brrise.png and b/assets/z_brrise.png differ diff --git a/assets/z_brw1fall1.png b/assets/z_brw1fall1.png index 1b7aebb..10ebd61 100644 Binary files a/assets/z_brw1fall1.png and b/assets/z_brw1fall1.png differ diff --git a/assets/z_brw1fall2.png b/assets/z_brw1fall2.png index 1762d26..dea3241 100644 Binary files a/assets/z_brw1fall2.png and b/assets/z_brw1fall2.png differ diff --git a/assets/z_brw1fall3.png b/assets/z_brw1fall3.png index 2443705..3927889 100644 Binary files a/assets/z_brw1fall3.png and b/assets/z_brw1fall3.png differ diff --git a/assets/z_brw1rise.png b/assets/z_brw1rise.png index d9dfd62..2fa0c22 100644 Binary files a/assets/z_brw1rise.png and b/assets/z_brw1rise.png differ diff --git a/assets/z_brw2fall1.png b/assets/z_brw2fall1.png index 37fa161..8013db0 100644 Binary files a/assets/z_brw2fall1.png and b/assets/z_brw2fall1.png differ diff --git a/assets/z_brw2fall2.png b/assets/z_brw2fall2.png index 7aaa7ca..f8efab1 100644 Binary files a/assets/z_brw2fall2.png and b/assets/z_brw2fall2.png differ diff --git a/assets/z_brw2fall3.png b/assets/z_brw2fall3.png index c7e5cef..2148409 100644 Binary files a/assets/z_brw2fall3.png and b/assets/z_brw2fall3.png differ diff --git a/assets/z_brw2rise.png b/assets/z_brw2rise.png index cc75f59..9c4b96e 100644 Binary files a/assets/z_brw2rise.png and b/assets/z_brw2rise.png differ diff --git a/assets/z_gkfall1.png b/assets/z_gkfall1.png index 298f371..713e11b 100644 Binary files a/assets/z_gkfall1.png and b/assets/z_gkfall1.png differ diff --git a/assets/z_gkfall2.png b/assets/z_gkfall2.png index 7dd4b9f..27b3691 100644 Binary files a/assets/z_gkfall2.png and b/assets/z_gkfall2.png differ diff --git a/assets/z_gkfall3.png b/assets/z_gkfall3.png index e90f0f1..c595eca 100644 Binary files a/assets/z_gkfall3.png and b/assets/z_gkfall3.png differ diff --git a/assets/z_gkrise.png b/assets/z_gkrise.png index efa9208..0adc587 100644 Binary files a/assets/z_gkrise.png and b/assets/z_gkrise.png differ diff --git a/assets/z_gkw1fall1.png b/assets/z_gkw1fall1.png index dd0e984..12d987c 100644 Binary files a/assets/z_gkw1fall1.png and b/assets/z_gkw1fall1.png differ diff --git a/assets/z_gkw1fall2.png b/assets/z_gkw1fall2.png index 3cf0f8c..0e07425 100644 Binary files a/assets/z_gkw1fall2.png and b/assets/z_gkw1fall2.png differ diff --git a/assets/z_gkw1fall3.png b/assets/z_gkw1fall3.png index f0afd57..5ec56e3 100644 Binary files a/assets/z_gkw1fall3.png and b/assets/z_gkw1fall3.png differ diff --git a/assets/z_gkw1rise.png b/assets/z_gkw1rise.png index f701018..59fc6c4 100644 Binary files a/assets/z_gkw1rise.png and b/assets/z_gkw1rise.png differ diff --git a/assets/z_gkw2fall1.png b/assets/z_gkw2fall1.png index 9181e94..3711d14 100644 Binary files a/assets/z_gkw2fall1.png and b/assets/z_gkw2fall1.png differ diff --git a/assets/z_gkw2fall2.png b/assets/z_gkw2fall2.png index 6734e64..9ffcf35 100644 Binary files a/assets/z_gkw2fall2.png and b/assets/z_gkw2fall2.png differ diff --git a/assets/z_gkw2fall3.png b/assets/z_gkw2fall3.png index f3a2add..fb2b47b 100644 Binary files a/assets/z_gkw2fall3.png and b/assets/z_gkw2fall3.png differ diff --git a/assets/z_gkw2rise.png b/assets/z_gkw2rise.png index 771c207..461cb86 100644 Binary files a/assets/z_gkw2rise.png and b/assets/z_gkw2rise.png differ diff --git a/assets/z_ljfall1.png b/assets/z_ljfall1.png index 9f333c9..1fd2c2d 100644 Binary files a/assets/z_ljfall1.png and b/assets/z_ljfall1.png differ diff --git a/assets/z_ljfall2.png b/assets/z_ljfall2.png index c2d3503..8b9e8bf 100644 Binary files a/assets/z_ljfall2.png and b/assets/z_ljfall2.png differ diff --git a/assets/z_ljfall3.png b/assets/z_ljfall3.png index 12bf269..75a695c 100644 Binary files a/assets/z_ljfall3.png and b/assets/z_ljfall3.png differ diff --git a/assets/z_ljrise.png b/assets/z_ljrise.png index 0d24646..05936e3 100644 Binary files a/assets/z_ljrise.png and b/assets/z_ljrise.png differ diff --git a/assets/z_ljw1fall1.png b/assets/z_ljw1fall1.png index 6fb715e..0eb7738 100644 Binary files a/assets/z_ljw1fall1.png and b/assets/z_ljw1fall1.png differ diff --git a/assets/z_ljw1fall2.png b/assets/z_ljw1fall2.png index 4dc8f2a..6186562 100644 Binary files a/assets/z_ljw1fall2.png and b/assets/z_ljw1fall2.png differ diff --git a/assets/z_ljw1fall3.png b/assets/z_ljw1fall3.png index fec6d33..03a419e 100644 Binary files a/assets/z_ljw1fall3.png and b/assets/z_ljw1fall3.png differ diff --git a/assets/z_ljw1rise.png b/assets/z_ljw1rise.png index be0af32..4539915 100644 Binary files a/assets/z_ljw1rise.png and b/assets/z_ljw1rise.png differ diff --git a/assets/z_ljw2fall1.png b/assets/z_ljw2fall1.png index 78e337f..1f79ab5 100644 Binary files a/assets/z_ljw2fall1.png and b/assets/z_ljw2fall1.png differ diff --git a/assets/z_ljw2fall2.png b/assets/z_ljw2fall2.png index b114d49..9c13b9e 100644 Binary files a/assets/z_ljw2fall2.png and b/assets/z_ljw2fall2.png differ diff --git a/assets/z_ljw2fall3.png b/assets/z_ljw2fall3.png index 7a7975b..68ae3fb 100644 Binary files a/assets/z_ljw2fall3.png and b/assets/z_ljw2fall3.png differ diff --git a/assets/z_ljw2rise.png b/assets/z_ljw2rise.png index 1d386f2..e6f3887 100644 Binary files a/assets/z_ljw2rise.png and b/assets/z_ljw2rise.png differ diff --git a/assets/z_rufall1.png b/assets/z_rufall1.png index 6ac6a5a..359f1b5 100644 Binary files a/assets/z_rufall1.png and b/assets/z_rufall1.png differ diff --git a/assets/z_rufall2.png b/assets/z_rufall2.png index 9657e85..4a9e024 100644 Binary files a/assets/z_rufall2.png and b/assets/z_rufall2.png differ diff --git a/assets/z_rufall3.png b/assets/z_rufall3.png index 888b492..46c8c05 100644 Binary files a/assets/z_rufall3.png and b/assets/z_rufall3.png differ diff --git a/assets/z_rurise.png b/assets/z_rurise.png index 51c0f11..e129a5a 100644 Binary files a/assets/z_rurise.png and b/assets/z_rurise.png differ diff --git a/assets/z_ruw1fall1.png b/assets/z_ruw1fall1.png index 3eaf451..f107e73 100644 Binary files a/assets/z_ruw1fall1.png and b/assets/z_ruw1fall1.png differ diff --git a/assets/z_ruw1fall2.png b/assets/z_ruw1fall2.png index 915c0fc..0a1a01f 100644 Binary files a/assets/z_ruw1fall2.png and b/assets/z_ruw1fall2.png differ diff --git a/assets/z_ruw1fall3.png b/assets/z_ruw1fall3.png index 0e7c216..f0db581 100644 Binary files a/assets/z_ruw1fall3.png and b/assets/z_ruw1fall3.png differ diff --git a/assets/z_ruw1rise.png b/assets/z_ruw1rise.png index a633090..0106622 100644 Binary files a/assets/z_ruw1rise.png and b/assets/z_ruw1rise.png differ diff --git a/assets/z_ruw2fall1.png b/assets/z_ruw2fall1.png index d39aff2..b22d882 100644 Binary files a/assets/z_ruw2fall1.png and b/assets/z_ruw2fall1.png differ diff --git a/assets/z_ruw2fall2.png b/assets/z_ruw2fall2.png index 1f8c30f..22864d4 100644 Binary files a/assets/z_ruw2fall2.png and b/assets/z_ruw2fall2.png differ diff --git a/assets/z_ruw2fall3.png b/assets/z_ruw2fall3.png index e49f5a1..9b34085 100644 Binary files a/assets/z_ruw2fall3.png and b/assets/z_ruw2fall3.png differ diff --git a/assets/z_ruw2rise.png b/assets/z_ruw2rise.png index 12fe379..dec63f7 100644 Binary files a/assets/z_ruw2rise.png and b/assets/z_ruw2rise.png differ diff --git a/assets/z_shfall1.png b/assets/z_shfall1.png index 8cd41e2..f27ce42 100644 Binary files a/assets/z_shfall1.png and b/assets/z_shfall1.png differ diff --git a/assets/z_shfall2.png b/assets/z_shfall2.png index ab0a999..f5346a2 100644 Binary files a/assets/z_shfall2.png and b/assets/z_shfall2.png differ diff --git a/assets/z_shfall3.png b/assets/z_shfall3.png index 17cf9ae..5fcaedd 100644 Binary files a/assets/z_shfall3.png and b/assets/z_shfall3.png differ diff --git a/assets/z_shrise.png b/assets/z_shrise.png index b40f914..d2a6a45 100644 Binary files a/assets/z_shrise.png and b/assets/z_shrise.png differ diff --git a/assets/z_shw1fall1.png b/assets/z_shw1fall1.png index cfe1bb0..27f926c 100644 Binary files a/assets/z_shw1fall1.png and b/assets/z_shw1fall1.png differ diff --git a/assets/z_shw1fall2.png b/assets/z_shw1fall2.png index 186f3e1..8453a8f 100644 Binary files a/assets/z_shw1fall2.png and b/assets/z_shw1fall2.png differ diff --git a/assets/z_shw1fall3.png b/assets/z_shw1fall3.png index 71d06d8..48abda0 100644 Binary files a/assets/z_shw1fall3.png and b/assets/z_shw1fall3.png differ diff --git a/assets/z_shw1rise.png b/assets/z_shw1rise.png index 3ef94fa..b63fa36 100644 Binary files a/assets/z_shw1rise.png and b/assets/z_shw1rise.png differ diff --git a/assets/z_shw2fall1.png b/assets/z_shw2fall1.png index c30ca9a..35cfe49 100644 Binary files a/assets/z_shw2fall1.png and b/assets/z_shw2fall1.png differ diff --git a/assets/z_shw2fall2.png b/assets/z_shw2fall2.png index 18a6d12..bdb56df 100644 Binary files a/assets/z_shw2fall2.png and b/assets/z_shw2fall2.png differ diff --git a/assets/z_shw2fall3.png b/assets/z_shw2fall3.png index 3c9d6ea..f792acb 100644 Binary files a/assets/z_shw2fall3.png and b/assets/z_shw2fall3.png differ diff --git a/assets/z_shw2rise.png b/assets/z_shw2rise.png index 59fd9a4..a4673b4 100644 Binary files a/assets/z_shw2rise.png and b/assets/z_shw2rise.png differ diff --git a/assets/z_stfall1.png b/assets/z_stfall1.png index 3351177..cefdc75 100644 Binary files a/assets/z_stfall1.png and b/assets/z_stfall1.png differ diff --git a/assets/z_stfall2.png b/assets/z_stfall2.png index 67e3342..f4ba98b 100644 Binary files a/assets/z_stfall2.png and b/assets/z_stfall2.png differ diff --git a/assets/z_stfall3.png b/assets/z_stfall3.png index 73dc188..1c670d2 100644 Binary files a/assets/z_stfall3.png and b/assets/z_stfall3.png differ diff --git a/assets/z_strise.png b/assets/z_strise.png index 4a0dcb8..e6e80f3 100644 Binary files a/assets/z_strise.png and b/assets/z_strise.png differ diff --git a/assets/z_stw1fall1.png b/assets/z_stw1fall1.png index bd11494..966a4de 100644 Binary files a/assets/z_stw1fall1.png and b/assets/z_stw1fall1.png differ diff --git a/assets/z_stw1fall2.png b/assets/z_stw1fall2.png index c130d0d..859c6d6 100644 Binary files a/assets/z_stw1fall2.png and b/assets/z_stw1fall2.png differ diff --git a/assets/z_stw1fall3.png b/assets/z_stw1fall3.png index 1094171..9b9b9c4 100644 Binary files a/assets/z_stw1fall3.png and b/assets/z_stw1fall3.png differ diff --git a/assets/z_stw1rise.png b/assets/z_stw1rise.png index 28b0356..ad484d4 100644 Binary files a/assets/z_stw1rise.png and b/assets/z_stw1rise.png differ diff --git a/assets/z_stw2fall1.png b/assets/z_stw2fall1.png index 041662d..34dbb4c 100644 Binary files a/assets/z_stw2fall1.png and b/assets/z_stw2fall1.png differ diff --git a/assets/z_stw2fall2.png b/assets/z_stw2fall2.png index cced90c..7e5163a 100644 Binary files a/assets/z_stw2fall2.png and b/assets/z_stw2fall2.png differ diff --git a/assets/z_stw2fall3.png b/assets/z_stw2fall3.png index 7b46460..c76e6fb 100644 Binary files a/assets/z_stw2fall3.png and b/assets/z_stw2fall3.png differ diff --git a/assets/z_stw2rise.png b/assets/z_stw2rise.png index 5a5e431..cfae8db 100644 Binary files a/assets/z_stw2rise.png and b/assets/z_stw2rise.png differ diff --git a/index.html b/index.html index 59ee12a..7d23661 100644 --- a/index.html +++ b/index.html @@ -431,6 +431,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { - + diff --git a/src/beats.js b/src/beats.js index bc4e04e..8a0a56a 100644 --- a/src/beats.js +++ b/src/beats.js @@ -364,7 +364,9 @@ function matchReady() { // fall3 is the one that HOLDS: "the opponent should fall and then lie there for a quick moment, the // player stands up too quick." A corpse that touches the ground and is instantly upright reads as a // stumble; the beat only lands if it is down long enough to look beaten. -const FALL = [['fall1', 0.14], ['fall2', 0.16], ['fall3', 0.62], ['rise', 0.36]]; +// "fall quicker. lie there for 2s then stand up." The collapse is now a drop — two frames in a fifth +// of a second — and the time is spent on the floor, where it reads. +const FALL = [['fall1', 0.09], ['fall2', 0.11], ['fall3', 2.0], ['rise', 0.30]]; export const FALL_LEN = FALL.reduce((s, f) => s + f[1], 0); function startFall(from) { @@ -468,7 +470,10 @@ function matchPoint(side) { aww(0.8); // ...and the umpire notices, drily. Situational, never a pool on a timer: the LOW line only // exists once the blood is nearly gone, so it lands as a warning rather than as commentary. - aside(G.hp <= 2 ? 'ump_low_blood' : (score.pts[1] % 2 ? 'ump_wound_you2' : 'ump_wound_you'), 0.7); + // AFTER the score call, not under it. voice() drops any line that would start while the umpire + // is still talking (one man, one microphone) — and matchPoint calls scoreKey() at 0.4 s with a + // clip over a second long, so every one of these was being swallowed silently. + aside(G.hp <= 2 ? 'ump_low_blood' : (score.pts[1] % 2 ? 'ump_wound_you2' : 'ump_wound_you'), 2.2); if (G.hp <= 0) return killPlayer(); // eaten — the run summary, as ever } const over = advancePoint(side); // banks the game and resets the points itself @@ -479,7 +484,7 @@ function matchPoint(side) { // A LIMB IS WORTH A REMARK. Two lines, alternating on the stage taken, so the same sentence // never lands twice in one game — a repeated aside reads as a stuck HUD, the same reason the // praise words cycle rather than roll. - aside(fellFrom ? 'ump_wound_opp2' : 'ump_wound_opp', 0.9); + aside(fellFrom ? 'ump_wound_opp2' : 'ump_wound_opp', 2.2); // ...after the score call, see above return startFall(fellFrom); // it goes down, and gets up short of a limb } matchReady(); // next point, same opponent, both on their marks @@ -495,7 +500,7 @@ function matchGameWon(zb, banked = false) { G.runGames++; // the ladder metric, same as every mode G.hp = Math.min(G.hpMax, G.hp + TUNE.matchHeal); // a clean game is how you last voice('game_player', 0.3); cheer(2.4, 0.6); - aside('ump_felled', 1.5); // ...over the applause, as he would + aside('ump_felled', 2.8); // ...after 'game', over the applause // GAME is the FLASH and the next opponent's name plate is the CARD. It used to be the other way // round and matchOpponent's plate landed on the same slot a frame later, so 'GAME.' never drew // once — and the plate is the better card anyway: it announces the win by replacing the loser. diff --git a/src/config.js b/src/config.js index 3c3cdcf..45da669 100644 --- a/src/config.js +++ b/src/config.js @@ -31,7 +31,12 @@ export const TUNE = { flickWidth: 3.95, // m off centre a PERFECTLY timed flick reaches. The singles line is 4.115, // so this is the corner with 17 cm to spare. Raising it past the line does // not make the flick punchier, it makes a perfect one go out. - flickReach: 0.42, // how much of an aimed shot's width a MISTIMED flick gets (1 = all of it). + flickReach: 0.62, // how much of an aimed shot's width a MISTIMED flick gets (1 = all of it). + // 0.42 made going out IMPOSSIBLE, which fixed the 8-in-10 bug and then + // overshot it: "flicking: too easy. i must be able to play out." At 0.62 + // a mistimed flick CAN sail (~15% of the time at quality 0, on the worst + // half of the scatter) and a perfectly timed one never can — the risk is + // back and it is timing, not a coin toss, that decides it. // Reach x quality must stay under the sideline once aimError is added on // top, or the flick goes out — which is exactly the bug this fixed: // 3.3 * 0.45 + 2.4 = 3.89 against a 4.11 m line. Raise it and the flick @@ -184,6 +189,10 @@ export const TUNE = { bossHopX: 2.0, // m of ground one hop covers (the eased arc lands it at about 2/pi of this) racketEvery: 3, // games between racket-zombie (plain opponent) spawns — was EVERY won game, which // is why he read as "the boss" and why a real boss would not have stood out + matchAsideEvery: 15, // ...but 55 s is a whole match game. In the match his asides are tied to + // things that happen every point or two — a limb, your blood — so the gore + // spacing muted him almost entirely. Same ONE cooldown mechanism, a value + // that fits the mode's own event rate. asideEvery: 55, // s between the umpire's sarcastic asides in splatter (jittered +-40%). He never // acknowledged that the match had changed; now he comments on it, disapprovingly. racketHpPerGame: 0.12, // the racket zombie stiffens as the run goes on (floored into whole hp) diff --git a/src/gameplay.js b/src/gameplay.js index c3b6d72..b752d6c 100644 --- a/src/gameplay.js +++ b/src/gameplay.js @@ -482,7 +482,7 @@ export function aside(name, delay = 0.2) { if (G.mode !== 'splatter' || G.modal || G.changeover || G.card) return; if (G.interlude && !G.match) return; if (G.asideT > 0) return; - G.asideT = TUNE.asideEvery; + G.asideT = G.match ? TUNE.matchAsideEvery : TUNE.asideEvery; voice(name, delay, 0.95); } diff --git a/src/main.js b/src/main.js index 00926d3..a7e546f 100644 --- a/src/main.js +++ b/src/main.js @@ -228,7 +228,13 @@ document.getElementById('nbSend').addEventListener('click', async () => { } }); nbName.addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('nbSend').click(); }); -document.getElementById('cStart').addEventListener('click', start); +// () => start(), NEVER `start` — addEventListener hands the listener a MouseEvent as its first +// argument, so `start(modeId = pickedMode)` never saw its default and modeId was an EVENT OBJECT. +// `G.classic = modeId === 'classic'` was therefore always false and the zombie branch never fired: +// SERVE UP dropped you into the gore mode whatever the setup screen said, and picking CLASSIC or +// ZOMBIE TENNIS did nothing at all. A default parameter only applies to `undefined`, and a click +// handler always passes something. +document.getElementById('cStart').addEventListener('click', () => start()); for (const id of ['sBack', 'stBack', 'rkBack', 'scBack']) document.getElementById(id).addEventListener('click', () => go('splash')); document.getElementById('bsBack').addEventListener('click', () => go('secret')); diff --git a/src/render.js b/src/render.js index 7abaef0..8583a64 100644 --- a/src/render.js +++ b/src/render.js @@ -195,7 +195,7 @@ export function proj(x, y, z) { // Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the // menu screens hang tags on the same files, and two copies of this number is one copy too // many: the one that does not get bumped is the one that serves a cached 404. -export const SPRV = 103; +export const SPRV = 104; // The boot bar in main.js watches this. Nothing here CHANGES when things load — every sprite was // already requested at import — it just makes the wait visible and holds the menu back until the // last sheet has landed. See the onerror note below for the other half of "sometimes missing". diff --git a/src/state.js b/src/state.js index b7c0f92..3e3dd80 100644 --- a/src/state.js +++ b/src/state.js @@ -198,7 +198,12 @@ export function setupPoint() { // Is there a stain worth walking out for? Must match mopRound's own target test, or he walks on, // finds nothing he can reach, and walks straight off again — which is exactly what Oliver saw: // "the mopboy sometimes moves over the court without anything to mop". -const mopWorthIt = () => gore.some(d => d.a >= 0.15 && d.z >= COURT.netZ - 2); +// ...and in a MATCH the mess is on the FAR side, because that is where the body stands and bleeds +// through four wound stages. This test was written for the gore mode, where everything dies on the +// near court, and it silently made the boy impossible in the match: sendBoy was called, found no +// stain it considered worth walking out for, and returned. The near-side bias is right everywhere +// else, so the match is the exception rather than the rule being widened. +const mopWorthIt = () => gore.some(d => d.a >= 0.15 && (G.match || d.z >= COURT.netZ - 2)); export function sendBoy(tx = ball.x, tz = ball.z, mop = G.mode === 'splatter') { // The sudden-death duel is clean tennis: nobody mops through it. THE MATCH IS THE EXCEPTION — it diff --git a/test.js b/test.js index 8c97100..d5eff7f 100644 --- a/test.js +++ b/test.js @@ -668,24 +668,29 @@ test('the first opponent cannot paint lines: its returns are width-clamped', () assert.ok(TUNE.matchWidePer > 0, 'and the width is earned back per rung'); }); -test('an AIMED return cannot be sent out by its own aim, at any timing', () => { +test('an aimed return can be played OUT, but only by mistiming it', () => { // The bug this pins: an aimed shot targeted a flat 3.3 m and THEN took err() on top — +-aimError // at quality 0 — so a flicked return landed as wide as 5.7 m against a 4.115 m singles line and // went out about eight times in ten, while an unaimed one (0 + the same scatter) was always in. // Flicking, the mode's one act of skill, was what lost the point. Reach scales with the same // quality the scatter shrinks with, so the two can never sum past the line — that sum is the // whole rule and it is arithmetic, so assert the arithmetic rather than a sampled rally. + // The contract is NOT "never out" — that was the overcorrection ("too easy. i must be able to + // play out"). It is: going out is possible and BOUNDED, it gets rarer as timing improves, and a + // perfectly timed flick can never do it. Risk decided by skill rather than by a coin toss. const line = COURT.W / 2, aimW = TUNE.flickWidth; // doHit's far-court aim width - for (let q = 0; q <= 1.0001; q += 0.05) { - const reach = TUNE.flickReach + (1 - TUNE.flickReach) * q; - const worst = aimW * reach + TUNE.aimError * (1 - q); - assert.ok(worst <= line, - `aimed reach + worst scatter must stay inside the line at quality ${q.toFixed(2)}: ` + - `${worst.toFixed(2)} vs ${line.toFixed(3)}`); - } - // ...and it must still be worth flicking: perfect timing keeps the full width. - assert.ok(aimW * (TUNE.flickReach + (1 - TUNE.flickReach)) > line * 0.75, - 'a perfectly timed flick still reaches the corner'); + const pOut = q => { + const reach = aimW * (TUNE.flickReach + (1 - TUNE.flickReach) * q); + const e = TUNE.aimError * (1 - q); + return e <= 0 ? 0 : Math.max(0, Math.min(1, (e - (line - reach)) / (2 * e))); + }; + assert.ok(pOut(0) > 0.02, 'a badly timed flick CAN go out: ' + (pOut(0) * 100).toFixed(1) + '%'); + assert.ok(pOut(0) < 0.25, '...but not the coin toss it was (every flick under q 0.66 went out)'); + assert.equal(pOut(1), 0, 'a perfectly timed flick never goes out'); + for (let q = 0.05; q <= 1.0001; q += 0.05) + assert.ok(pOut(q) <= pOut(q - 0.05) + 1e-9, `risk falls as timing improves (q ${q.toFixed(2)})`); + // ...and it must still be worth flicking: perfect timing reaches the corner. + assert.ok(aimW > line * 0.9, 'a perfectly timed flick still reaches the corner'); }); test('a wounding point knocks the body down and it gets up MINUS the limb', () => {