The leagues, phase 2: five bands, pace tiers, locked courts, the members' ground, the promotion ceremony

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-29 12:43:23 +02:00
1 parent 9133c0d351
commit 16bf5b9fed
12 files changed
+406 -71

No files matched your search

+13 -7
View File
@@ -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, matchWound, CUP_ROUNDS, LADDER } 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, LADDER } 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, voiceSeq, sfx, aww, zombieMusic, zombieDuck, menuDuck, splat, shriek, boom } from './audio.js';
import { haptic } from './haptics.js';
@@ -158,6 +158,12 @@ export const swayOf = aim => aim.mag === undefined ? aim.ax
// as flat rather than resetting the lesson it just failed to use.
export const flatCount = (streak, aim) => (aim && Math.abs(swayOf(aim)) > 0.15 ? 0 : streak + 1);
// THE LEAGUES' pace tier: solo match modes run faster per league, everything else is factor 1 —
// league 1 IS the shipped game (the calibration rule). Multiplies ball pace on BOTH rackets
// (doHit, both serve paths) and the opponent's legs (rzChaseSpeed), so the world speeds up as one
// piece and the reach tests keep believing the legs they measure. Netplay is a human and exempt.
export const leaguePace = () => G.match && !G.net ? 1 + (G.league - 1) * TUNE.leaguePace : 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';
@@ -173,7 +179,7 @@ function doHit(ch, quality, aim) {
ball.z = ch.z + (toFar ? -0.3 : 0.3);
const err = () => (Math.random() * 2 - 1) * TUNE.aimError * (1 - quality);
let tx, tz, aimedLane = false, sway = 0;
let s = TUNE.baseShotSpeed + quality * TUNE.qualityShotSpeed;
let s = (TUNE.baseShotSpeed + quality * TUNE.qualityShotSpeed) * leaguePace();
const ft = G.mode !== 'tennis' && !G.interlude && ch.side === 'P' // the duel aims like tennis: lanes, not free-aim
? (aim && aim.fx !== undefined ? flickTarget(ch, aim) : { zt: zombieTarget(aim) }) : null;
if (ft && ft.zt) {
@@ -379,7 +385,7 @@ function splatServeTarget(sv, aim, s, x0, z0, ex = 0) {
// The launch itself: topspin gravity, then trade pace for arc until the net is genuinely cleared.
// A shambler just the other side of the tape is only reachable on a loft.
function serveArc(x0, y0, z0, tx, tz, quality, ty = 0.08, hard = 0) { // hard: the aimed strike's 0..1
let s = TUNE.serveSpeed + quality * TUNE.serveQualitySpeed + hard * TUNE.serveHard;
let s = (TUNE.serveSpeed + quality * TUNE.serveQualitySpeed + hard * TUNE.serveHard) * leaguePace();
// the harder you hit it, the harder it dips — and the HARD serve dives harder still
// (serveHardSpin), which is what buys its net clearance: MEASURED with the replica solver,
// +9 m/s with the old dip was cut BELOW the plain flush by the loft loop (47.2 vs 49.3).
@@ -468,7 +474,7 @@ export function hitServe(sv, quality, aim = null, perfect = quality > 0.999) {
const sx = aim && aim.sx !== undefined ? aim.sx : 0;
const hard = (aim && aim.hard) || 0;
const pace = hard * TUNE.serveHard;
const s = TUNE.serveSpeed + quality * TUNE.serveQualitySpeed + pace;
const s = (TUNE.serveSpeed + quality * TUNE.serveQualitySpeed + pace) * leaguePace();
// splatServeTarget + serveArc are shared with previewServe, so the arc drawn under your thumb
// is the one the ball flies. Only the aim error is added here and left out of the preview.
const st = splatServeTarget(sv, aim, s, ball.x, ball.z, (Math.random() * 2 - 1) * (1 - quality) * 0.8);
@@ -1126,10 +1132,10 @@ function matchFallWillPlay() {
const cup = G.match.cup;
const gameWin = score.pts[0] + 1 >= 4 && score.pts[0] + 1 - score.pts[1] >= 2;
// a game win is only THE KILL when it ends the round — a cup mid-round game keeps the body
if (gameWin) return !cup || cup.g + 1 >= CUP_ROUNDS[cup.round].need;
if (gameWin) return !cup || cup.g + 1 >= cup.rounds[cup.round].need;
// the same divisor matchPoint will use — the first version kept the deathmatch /4 and in the
// cup it promised falls that matchPoint then didn't deliver (a route to the stale-latch lock)
const div = 4 * ((cup && CUP_ROUNDS[cup.round].need) || 1);
const div = 4 * ((cup && cup.rounds[cup.round].need) || 1);
const after = Math.max(1, rz.hp - Math.ceil(rz.hpMax / div));
return racketStage({ hp: after, hpMax: rz.hpMax }) !== racketStage(rz); // a limb
}
@@ -1145,7 +1151,7 @@ function matchFallWillPlay() {
// One definition, because the read boost, the wrong-foot slow and the surprise whiff must all
// answer "how smart is this one" identically or the twist pulls in three directions at once.
const learnK = rz => G.match && !G.net ? Math.min(1, Math.max(0, (G.match.opp - 1) / (LADDER - 1))) : 0;
const rzChaseSpeed = rz => TUNE.racketSpeed
const rzChaseSpeed = rz => TUNE.racketSpeed * leaguePace()
* (G.match ? (TUNE.matchSpeedBase + (G.match.opp - 1) * TUNE.matchSpeedPer) * ((rz.foe && rz.foe.speed) || 1) : 1)
* (G.wasServe && ball.lastHit === 'P' ? TUNE.aiReturnDash : 1)
// wrong-footed: a body mid-lean is slow to turn — THE READ's one-ball cost, and it lives HERE