The body plays the wounds it wears, and the flick finally has a teacher
Score-is-the-body was a picture: the opponent lost an arm and swung exactly as far, lost its head and missed exactly as often. Two dials now read the same racketStage the SKINS are picked with, so what you watch it lose is what it loses — deliberately small (6% reach, 17.5% whiff per stage), texture under the ladder rather than a second difficulty curve, with a test on that ceiling. An unhurt body still returns exactly 1 and 1, and the whole thing is gated on G.match: the gore duel's racket zombie is untouched. And the placement flick, the mode's one act of skill, was taught nowhere on touch — the placement ring is a receipt, not a lesson, and you only get one if you already knew to swipe. The ghost hand teaches it now, in the same register as PULL DOWN and TAP AS IT ARRIVES, riding the failure it answers: a run of returns that left with no aim on them. The tap lesson still wins when both qualify — placement is the wrong thing to teach someone who cannot reach the ball. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0c90337f2d
commit
c613b6ceba
7 files changed
+147
-21
No files matched your search
+19
-4
@@ -1,6 +1,6 @@
|
||||
// Game logic: ballistics, swings, serves, rules/scoring, AI, and the per-frame update.
|
||||
|
||||
import { TUNE, UI, bossArt, APPLAUSE_T, EATEN_BURST, EATEN_SUMMARY, COURT, CONTACT, VOLLEY, ZONE_DEPTH, HOME, P_BOUNDS, TOSS, SHOT_DEPTH, POSE_TIME, BOUNCE_IDLE, zombieH, Z_CHEST , WEAPONS, RACK_MAX , BARREL_WEAPONS, racketStage, CUP_ROUNDS } from './config.js';
|
||||
import { TUNE, UI, bossArt, APPLAUSE_T, EATEN_BURST, EATEN_SUMMARY, COURT, CONTACT, VOLLEY, ZONE_DEPTH, HOME, P_BOUNDS, TOSS, SHOT_DEPTH, POSE_TIME, BOUNCE_IDLE, zombieH, Z_CHEST , WEAPONS, RACK_MAX , BARREL_WEAPONS, racketStage, matchWound, CUP_ROUNDS } from './config.js';
|
||||
import { P, O, ball, score, crowd, G, court, tour, other, deuceSide, setupPoint, sendBoy, resetState } from './state.js';
|
||||
import { audio, pock, swish, whoosh, applause, voice, sfx, aww, zombieMusic, zombieDuck, menuDuck, splat, shriek, boom } from './audio.js';
|
||||
import { axisX, axisZ, takeFlickAim, touchPX, resetInput } from './input.js';
|
||||
@@ -123,10 +123,21 @@ function zombieTarget(aim) {
|
||||
return best;
|
||||
}
|
||||
|
||||
// A return that left with a direction on it resets the flick lesson; a flat one is one more piece of
|
||||
// evidence that the player does not know the gesture is there. Exported for the suite — the touch
|
||||
// flick cannot be pressed headlessly, and this rule is the whole of the lesson's trigger.
|
||||
export const flatCount = (streak, aim) => (aim && aim.ax ? 0 : streak + 1);
|
||||
|
||||
function doHit(ch, quality, aim) {
|
||||
// aim: {ax: -1|0|1, az: -1|0|1} from held keys / flick at contact (az -1 = deep, 1 = short)
|
||||
const toFar = ch.side === 'P';
|
||||
if (ch.side === 'P') G.passStreak = 0; // a landed swing ends the tap-to-hit lesson
|
||||
if (ch.side === 'P') {
|
||||
G.passStreak = 0; // a landed swing ends the tap-to-hit lesson
|
||||
// ...and a landed swing WITH A DIRECTION ON IT ends the flick lesson. Counted here rather than
|
||||
// at the gesture because a flick that arrives too late to be read is a flick that taught the
|
||||
// player nothing: what matters is whether the ball left with an aim on it. Held keys count too.
|
||||
G.flatStreak = flatCount(G.flatStreak, aim);
|
||||
}
|
||||
// snap the ball to the racket at the contact frame so the hit visually connects
|
||||
ball.x = ch.x + ch.facing * 0.5;
|
||||
ball.z = ch.z + (toFar ? -0.3 : 0.3);
|
||||
@@ -1322,12 +1333,16 @@ function racketAI(rz, dt) {
|
||||
earnFlip(rz, 'face', Math.abs(dx) > 0.2 ? (dx > 0 ? 1 : -1) : 0, dt);
|
||||
rz.vx = dx / d * step / dt;
|
||||
} else { rz.vx = 0; if (rz.pose === 'run') rz.pose = 'idle'; }
|
||||
// THE WOUNDS PLAY. Score-is-the-body was a picture until now: it lost an arm and swung exactly
|
||||
// as far, lost its head and missed exactly as often. Both dials read the same racketStage the
|
||||
// SKINS do, so what you see it lose is what it loses. Match only — see config.matchWound.
|
||||
const wnd = G.match ? matchWound(rz) : { reach: 1, flub: 1 };
|
||||
if (rz.swingCool <= 0 && rz.swingT < 0
|
||||
&& Math.hypot(ball.x - rz.x, (ball.z - rz.z) * 0.8) < TUNE.aiReach
|
||||
&& Math.hypot(ball.x - rz.x, (ball.z - rz.z) * 0.8) < TUNE.aiReach * wnd.reach
|
||||
&& ball.y < CONTACT.maxY && ball.y > CONTACT.minY) {
|
||||
// ...and the match has its OWN flub base: the duel's 0.2 hands a fifth of all returns away,
|
||||
// which across a game is an opponent that beats itself.
|
||||
const flub = G.match ? TUNE.matchFlub * Math.pow(TUNE.matchFlubKeep, G.match.opp - 1) * (rz.foe?.flub || 1)
|
||||
const flub = G.match ? TUNE.matchFlub * Math.pow(TUNE.matchFlubKeep, G.match.opp - 1) * (rz.foe?.flub || 1) * wnd.flub
|
||||
: TUNE.racketFlub;
|
||||
if (Math.random() < flub) rz.swingCool = 0.5; // whiff — it hesitates (rarer per rung of the ladder)
|
||||
else { rz.swingT = TUNE.swingWindup + TUNE.swingWindow / 2; trySwing(rz, true, TUNE.racketQMax); rz.swingT = -1; rz.swingCool = 0.3; }
|
||||
|
||||
Reference in new issue
Block a user