Steady the gold ring, and move the praise word off the ball
Oliver: the target ring twitches sometimes, and "shot of the day", "textbook" etc. cover the ball. The timed ring re-solved every frame and a small shift in the auto-run's target moved its moment by up to 158 ms in a frame; the drawn cue now counts down on its own clock and eases toward each solve (p99 jump 100 -> 17 ms). The rally's praise word rose straight into the struck ball's path (and a serve's sat on the far bounce, where the return comes back); it now lands under the player, behind his feet, and stays put. Same moment, out of the way. v371. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8e4b298d92
commit
d561050af8
7 files changed
+50
-8
No files matched your search
+16
@@ -5515,6 +5515,22 @@ taken earlier, on the rise, while it is still inside. The final target is clampe
|
||||
ball still gets a run to the nearest legal spot. Test: his ball (hard cross-court, lands at 3.3, kicks
|
||||
out). Without the fix the target is 14.8 m; with it the run stays in bounds and he returns it.
|
||||
|
||||
### The ring stops twitching, and the praise word gets out of the ball's way (2026-10-14, v371)
|
||||
Oliver: *"der Treffer-/Zielkreis zittert jetzt manchmal, außerdem ... die Nachrichten wie shot of the
|
||||
day, textbook ... verdecken manchmal den Ball."*
|
||||
- **The timed ring re-solved every frame** against the auto-run's target, and a target shifting a few
|
||||
centimetres moved the predicted moment. Probe: p99 100 ms, max 158 ms per frame, nearly half the
|
||||
ring's size in one frame. The drawn cue is now `G.ring`, smoothed in `update`: it counts down on its
|
||||
own clock and eases toward each fresh solve (`ringSmooth` 8/s), and a frame with no solve holds it
|
||||
for up to 0.12 s. Measured jump: p99 17 ms, max 21 ms. Gold toggles are unchanged (two per ball).
|
||||
The bots and tests keep reading the raw `ringCue`.
|
||||
- **The praise word sat ON the ball.** On a rally contact it rose straight into the path of the ball
|
||||
just struck; a serve's word sat on the far bounce, where the return comes back through.
|
||||
`praisePop`: in tennis modes it lands `praiseBehind` (1 m) behind the player's feet, on the dark
|
||||
apron under him, and stays put (`popText`'s new `still`). The MOMENT is unchanged (contact / the
|
||||
serve's landing). Gore keeps its words on the bodies they belong to. Checked in the pane: PERFECT
|
||||
under him, ball clear.
|
||||
|
||||
# OPEN — leftovers, in rough priority order
|
||||
|
||||
Nothing here is broken; these are known gaps and unverified risks.
|
||||
|
||||
+1
-1
@@ -1140,6 +1140,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
||||
</div>
|
||||
</div>
|
||||
<span id="recLamp" hidden>REC</span>
|
||||
<script type="module" src="src/main.js?v=370"></script>
|
||||
<script type="module" src="src/main.js?v=371"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -648,6 +648,8 @@ export const TUNE = {
|
||||
// the swing window, at any pace. 0 = the old distance ring (matchRingSweet/Band)
|
||||
ringBand: 0.35, // share of the half-window either side of the sweet moment that draws gold (edge = q 0.65)
|
||||
ringLead: 0.9, // s before contact the timed ring appears
|
||||
ringSmooth: 8, // 1/s: how fast the drawn ring eases toward each fresh solve (it counts down on its own)
|
||||
praiseBehind: 1.0, // m behind the player's feet a rally's praise word is drawn (under him, off the ball's path)
|
||||
flickLead: 0, // s a flick's swing is back-dated toward touch-down (v366 used 0.1 — his next log showed
|
||||
// every 62-64 ms flick landing ~85 ms EARLY: a flick is timed by its swipe, not its touch)
|
||||
// ---- THE READ (2026-08-28, Oliver: past the HEAD COACH "the opponents are very fast and barely
|
||||
|
||||
+25
-2
@@ -87,6 +87,16 @@ let praiseN = 0;
|
||||
// the word must land at the IMPACT, not at the release (Oliver: "the reactions are playing after
|
||||
// shooting the ball, not when the ball hits"). A serve arms it on ball.praise and the contact/
|
||||
// bounce pops it; a rally hit pops it at the racket, where contact and praise are the same moment.
|
||||
// ...and in a rally it lands BELOW THE PLAYER, just behind his feet, and stays put (Oliver, 2026-10-14:
|
||||
// "die Nachrichten wie shot of the day, textbook verdecken manchmal den Ball"). On the contact it rose
|
||||
// straight into the path of the ball he had just struck; a serve's word sat on the far bounce, which
|
||||
// is exactly where the return comes back through. The MOMENT is unchanged — only where it is drawn.
|
||||
// Gore keeps its words on the bodies they belong to.
|
||||
function praisePop(w, x, y, z) {
|
||||
if (rallyFeel() || G.mode === 'tennis') popText(P.x, 0, P.z + TUNE.praiseBehind, w, undefined, true);
|
||||
else popText(x, y, z, w);
|
||||
}
|
||||
|
||||
function praiseWord() {
|
||||
if (G.praiseT > 0) return null;
|
||||
G.praiseT = TUNE.praiseEvery;
|
||||
@@ -410,7 +420,7 @@ function doHit(ch, quality, aim) {
|
||||
if (quality > 0.999 && ch.side === 'P') { // the rally hit: a real 45 ms window (perfectMs)
|
||||
G.hitstop = 0.05; pock(0.5, 1600);
|
||||
const w = praiseWord(); // a rally hit IS the impact — the word pops right on the contact
|
||||
if (w) popText(ball.x, ball.y, ball.z, w);
|
||||
if (w) praisePop(w, ball.x, ball.y, ball.z);
|
||||
}
|
||||
ch.pose = 'swing'; ch.poseT = 0;
|
||||
if (G.net && ch.side === 'P') netSendShot(); // netplay: the shot IS the sync unit
|
||||
@@ -1320,7 +1330,7 @@ function onBounce() {
|
||||
pock(0.35, 500);
|
||||
if (ball.scored) return; // a called ball still bounces and still sounds; it just cannot score twice
|
||||
// a deferred praise word that met no body lands with the ball — the DRAW was still perfect
|
||||
if (ball.praise) { popText(ball.x, 0.3, ball.z, ball.praise); ball.praise = null; }
|
||||
if (ball.praise) { praisePop(ball.praise, ball.x, 0.3, ball.z); ball.praise = null; }
|
||||
if (G.mode === 'splatter' && ball.bomb && ball.bounces === 1) { // a bomb that missed everyone still blows up
|
||||
explode(ball.x, ball.z); ball.bomb = false;
|
||||
spentBall(); return;
|
||||
@@ -2306,6 +2316,19 @@ export function update(dt) {
|
||||
// mid-stride left him welded to a single run frame — the "stuck mid-animation" report.
|
||||
// vx/vz die with the mover too, or the speed ghosts would trail a body that stopped dead.
|
||||
else { P.vx = 0; P.vz = 0; if (P.pose === 'run' && G.dodgeT <= 0) { P.pose = 'idle'; P.poseT = 0; } }
|
||||
// THE GOLD RING, STEADY (Oliver, 2026-10-14: "der Treffer-/Zielkreis zittert jetzt manchmal").
|
||||
// ringCue re-solves every frame against where the auto-run is taking him, and a target that shifts
|
||||
// a few centimetres moved the predicted moment by a frame or two — the ring's size twitched and its
|
||||
// gold blinked. The drawn cue COUNTS DOWN on its own clock and only eases toward each fresh solve
|
||||
// (TUNE.ringSmooth), and a frame with no solve holds it instead of dropping it.
|
||||
if (G.phase === 'rally' && ball.active && ball.lastHit === 'O' && (rallyFeel() || G.mode === 'tennis')) {
|
||||
const c = ringCue();
|
||||
if (c && !G.ring) G.ring = { ...c };
|
||||
else if (c) { const e = G.ring.eta - dt; G.ring = { ...c, eta: e + (c.eta - e) * Math.min(1, dt * TUNE.ringSmooth) }; }
|
||||
else if (G.ring && (G.ring.hold = (G.ring.hold || 0) + dt) < 0.12) G.ring.eta -= dt;
|
||||
else G.ring = null;
|
||||
if (G.ring) G.ring.gold = Math.abs(G.ring.eta - G.ring.sweet) < G.ring.band;
|
||||
} else G.ring = null;
|
||||
// The sidestep runs in EVERY phase — that is the entire point of it, since a boss fight is spent
|
||||
// in 'pretoss' where the block above never runs. Same clamp as the walk, so it cannot carry him
|
||||
// off the edge of the screen either.
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ import { cv, ctx } from './dom.js';
|
||||
import { BENCH, BENCH_STAGES, ZFAMS, famFrames, racketStage, TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, DONE_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW, TOSS, HEAD_CROP, TINT, SHOT_DEPTH, matchFoe, cupFixture, cupRoundName } from './config.js';
|
||||
import { P, O, ball, score, crowd, G, court, tour, kit, skin } from './state.js';
|
||||
import { RAISERS, FRAMES } from './crowd.js';
|
||||
import { predictLanding, previewServe, slotId, slotStock, serveApexY, rallyFeel, swayOf, ringCue } from './gameplay.js';
|
||||
import { predictLanding, previewServe, slotId, slotStock, serveApexY, rallyFeel, swayOf } from './gameplay.js';
|
||||
import { drillQueue, drillLegal, drillMult } from './drill.js';
|
||||
import { zombies, gore, piles, parts, pellets, extras, shots, pops } from './zombies.js';
|
||||
import { runDelta, progress, prof, cupBest, hasPromo } from './profile.js';
|
||||
@@ -1075,7 +1075,7 @@ function drawApproachRing() {
|
||||
const rSweet = Math.max(6, p.s * 0.055); // the fixed target ring
|
||||
let inBand, r;
|
||||
if (TUNE.ringTime) { // TIMED (gameplay.ringCue): kiss = touch now
|
||||
const c = ringCue();
|
||||
const c = G.ring; // gameplay's STEADY copy of ringCue
|
||||
if (!c || c.eta > TUNE.ringLead || c.eta < c.sweet - c.band) return; // not yet, or the moment has passed
|
||||
inBand = c.gold;
|
||||
r = rSweet * (1 + Math.max(0, c.eta - c.sweet) * 4); // closes onto the target as the moment nears
|
||||
@@ -2883,7 +2883,7 @@ export function render() {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = Math.min(1, f * 3); // holds, then a quick fade at the end
|
||||
ctx.font = B(13); ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||||
shadowText(p.text, Math.round(pr.x), Math.round(pr.y - (1 - f) * 14), p.col || '#ffffff');
|
||||
shadowText(p.text, Math.round(pr.x), Math.round(pr.y - (p.still ? 0 : (1 - f) * 14)), p.col || '#ffffff');
|
||||
ctx.restore();
|
||||
}
|
||||
// (a PERFECT-contact spark ring lived here for one build — Oliver's verdict: too much. The
|
||||
|
||||
@@ -184,6 +184,7 @@ export const G = {
|
||||
rallyNear: null, // closest the incoming ball got this rally (for never-swung passes)
|
||||
swung: false, // player swung at least once this rally — whiff log supersedes PASSED
|
||||
lastMiss: null, // {text, at} — most recent miss reason, drawn on the HUD briefly
|
||||
ring: null, // the drawn gold ring's steady cue (gameplay update smooths ringCue)
|
||||
logShow: false, // tap the scoreboard to toggle the on-screen miss-log dump
|
||||
};
|
||||
|
||||
|
||||
+2
-2
@@ -29,8 +29,8 @@ export const shots = []; // {x, y, z, vx, vy, vz, g, t}
|
||||
// the impact — the answer to "what did I just do" appears where you are already looking, not in
|
||||
// the announcement band. Render draws them, gameplay's update ticks them, killed in resetZombies.
|
||||
export const pops = []; // {x, y, z, text, col, t, T}
|
||||
export function popText(x, y, z, text, col) {
|
||||
pops.push({ x, y, z, text, col, t: 0.85, T: 0.85 });
|
||||
export function popText(x, y, z, text, col, still = false) {
|
||||
pops.push({ x, y, z, text, col, still, t: 0.85, T: 0.85 }); // still: does not rise (the rally's praise, under the player)
|
||||
if (pops.length > 10) pops.shift();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user