Sharpen league and Climb difficulty and give Gore a front door
This commit is contained in:
1 parent
e3faeb0a3a
commit
ede1f48152
6 files changed
+151
-51
No files matched your search
+4
-4
@@ -1250,19 +1250,19 @@ const OPPONENTS = [
|
||||
line: 'HE GAVE LESSONS ON THIS COURT.', speed: 1.16, pace: 1.22, wide: 1.20, flub: 0.70 },
|
||||
// THE BRUTE, not "THE BULK": the gore mode's stats screen has called this body THE BRUTE since it
|
||||
// was the pole-dragger (ENEMIES.g below), and one character may not carry two names.
|
||||
// Final-league returns hunt the open court (smart) and have individual quality caps. Earlier
|
||||
// Final-league returns hunt the open court (smart) and have individual quality bands. Earlier
|
||||
// opponents keep random lanes and racketQMax. Brute's legs must clear even league 4's fastest.
|
||||
{ short: 'BRUTE', name: 'THE BRUTE', fam: 'br', h: 2.0,
|
||||
line: 'NOTHING GETS PAST HIM TWICE.', speed: 1.12, pace: 1.34, wide: 1.24, flub: 0.66, smart: 0.20, qMax: 0.60 },
|
||||
line: 'NOTHING GETS PAST HIM TWICE.', speed: 1.12, pace: 1.34, wide: 1.24, flub: 0.66, smart: 0.50, qMin: 0.62, qMax: 0.76 },
|
||||
{ short: 'DIRECTOR', name: 'THE DIRECTOR', fam: 'dr',
|
||||
line: 'HE CAN HAVE YOU REMOVED.', speed: 1.26, pace: 1.28, wide: 1.28, flub: 0.60, smart: 0.40, qMax: 0.65 },
|
||||
line: 'HE CAN HAVE YOU REMOVED.', speed: 1.26, pace: 1.28, wide: 1.28, flub: 0.60, smart: 0.70, qMin: 0.68, qMax: 0.83 },
|
||||
// 18: the man whose name is on the honours board. `h` is WORLD height (zombieH reads it, so the
|
||||
// serve aims at the body actually drawn): four family rolls could not make the art itself bulky
|
||||
// against the pose sheet — see _famroll.py's pair-reference note — so his size is stature.
|
||||
// Six new rungs stand between him and the start now, so he took the step up Oliver asked for:
|
||||
// he reads the court faster, hits harder, paints wider, and misses half as often as he did.
|
||||
{ short: 'CHAMPION', name: 'THE CHAMPION', fam: 'ch', h: 2.1,
|
||||
line: 'HE RETURNS EVERYTHING.', speed: 1.45, pace: 1.42, wide: 1.36, flub: 0.38, smart: 0.60, qMax: 0.70 },
|
||||
line: 'HE RETURNS EVERYTHING.', speed: 1.45, pace: 1.42, wide: 1.36, flub: 0.38, smart: 0.85, qMin: 0.74, qMax: 0.90 },
|
||||
];
|
||||
|
||||
// Who opponent N is. This used to reuse the chapter roster's cycling (bossArt) so the two lists
|
||||
|
||||
+14
-8
@@ -275,7 +275,7 @@ function doHit(ch, quality, aim) {
|
||||
// rungs take it back.
|
||||
// ...and the RUNG's own pace trait rides on top, inside the cap: full pace is still the ceiling,
|
||||
// an opponent just gets there sooner or later than the flat curve would (see matchFoe in config).
|
||||
s *= Math.min(1, (TUNE.matchOppPace + (G.match.opp - 1) * TUNE.matchOppPacePer) * (ch.foe?.pace || 1));
|
||||
s *= Math.min(1, (TUNE.matchOppPace + matchStep() * TUNE.matchOppPacePer) * (ch.foe?.pace || 1));
|
||||
}
|
||||
if (G.match) {
|
||||
// THE OPPONENT'S WIDTH IS THE THIRD DIAL. Its returns scatter off aimError x (1 - 0.55
|
||||
@@ -283,7 +283,7 @@ function doHit(ch, quality, aim) {
|
||||
// balls". Opponent one keeps its returns inside matchWideBase of the centre line; each rung
|
||||
// earns matchWidePer more width, so the line-painting is the LATE ladder, not the opening one.
|
||||
if (ch.side !== 'P') {
|
||||
const wide = (TUNE.matchWideBase + (G.match.opp - 1) * TUNE.matchWidePer) * (ch.foe?.wide || 1);
|
||||
const wide = (TUNE.matchWideBase + matchStep() * TUNE.matchWidePer) * (ch.foe?.wide || 1);
|
||||
tx = Math.max(-wide, Math.min(wide, tx));
|
||||
}
|
||||
}
|
||||
@@ -1172,8 +1172,15 @@ 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;
|
||||
// The Climb is a repeatable arcade run: less nursery at the bottom. Four extra progression
|
||||
// steps taper to zero at opponent eight; identities, territory tempo and cup onboarding stay put.
|
||||
// Share this across movement, intercept planning and returns so all four dials agree.
|
||||
const matchStep = () => {
|
||||
const n = G.match.opp - 1;
|
||||
return n + (!G.net && !G.match.cup && !G.match.train ? 4 * Math.max(0, 1 - n / 7) : 0);
|
||||
};
|
||||
const rzChaseSpeed = rz => TUNE.racketSpeed * leaguePace()
|
||||
* (G.match ? (TUNE.matchSpeedBase + (G.match.opp - 1) * TUNE.matchSpeedPer) * ((rz.foe && rz.foe.speed) || 1)
|
||||
* (G.match ? (TUNE.matchSpeedBase + matchStep() * TUNE.matchSpeedPer) * ((rz.foe && rz.foe.speed) || 1)
|
||||
: 1 + TUNE.duelSpeedMax * duelSkill())
|
||||
* (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
|
||||
@@ -1456,10 +1463,9 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
|
||||
: [ -1, 0, 1 ][Math.floor(Math.random() * 3)],
|
||||
az: Math.random() < 0.25 ? -1 : 0 }
|
||||
: flick || { ax: axisX(), az: axisZ() * (ch.side === 'P' ? 1 : -1) };
|
||||
// G.diff (classic difficulty) moves the FLOOR too: a raised qMin is what makes HARD's every
|
||||
// ball arrive with pace, where a cap alone just trims the best ones. Null outside classic, so
|
||||
// the racket zombie and the gore opener keep the TUNE floor.
|
||||
const qMin = G.diff ? G.diff.qMin : TUNE.aiQualityMin;
|
||||
// A quality floor makes strong opponents sustain pressure, instead of only improving their
|
||||
// luckiest returns. Classic and solo match traits own their bands; gore keeps its own floor.
|
||||
const qMin = G.diff ? G.diff.qMin : G.match && !G.net ? (ch.foe?.qMin ?? TUNE.aiQualityMin) : TUNE.aiQualityMin;
|
||||
if (isAI) q = Math.min(q, qMin + Math.random() * (Math.max(qCap, qMin) - qMin));
|
||||
if (!isAI && G.mode === 'tennis') {
|
||||
G.swingSnaps = null; G.rallyNear = null;
|
||||
@@ -1540,7 +1546,7 @@ function racketAI(rz, dt) {
|
||||
&& 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) * wnd.flub
|
||||
const flub = (G.match ? TUNE.matchFlub * Math.pow(TUNE.matchFlubKeep, matchStep()) * (rz.foe?.flub || 1) * wnd.flub
|
||||
: TUNE.racketFlub * (1 - TUNE.duelFlubCut * duelSkill()))
|
||||
// THE READ: a wrong-footed racket reaches the ball late even when the legs got there —
|
||||
// the surprise ball ADDS whiff on top of the rung's own rate, scaled by how much this
|
||||
|
||||
+10
-18
@@ -600,13 +600,12 @@ const countUp = (write, target, dur = 700) => {
|
||||
};
|
||||
// A SHIELD CATCHES THE LIGHT (2026-08-31, Oliver: not burning — "manchmal random schimmern").
|
||||
// The first cut was a chimney of embers, which read as a fire on a trophy. This is the opposite:
|
||||
// three points ON the shield, each dark for most of a long cycle and flaring for a fraction of it,
|
||||
// on delays and durations that share no common multiple — so a glint lands somewhere on the row
|
||||
// every couple of seconds and never twice in the same rhythm. CSS keyframes, no game loop.
|
||||
const glints = won => !won ? '' : Array.from({ length: 3 }, () =>
|
||||
// Three points ON the shield: the first catches the light within a second of opening the card,
|
||||
// then the long, varied cycles keep it occasional. CSS keyframes, no game loop.
|
||||
const glints = won => !won ? '' : Array.from({ length: 3 }, (_, i) =>
|
||||
`<em style="left:${(18 + Math.random() * 60).toFixed(0)}%;` +
|
||||
`top:${(12 + Math.random() * 52).toFixed(0)}%;` +
|
||||
`animation-delay:${(Math.random() * 9).toFixed(2)}s;` +
|
||||
`animation-delay:${(0.15 + i * 0.6 + Math.random() * 0.3).toFixed(2)}s;` +
|
||||
`animation-duration:${(6 + Math.random() * 5).toFixed(2)}s"></em>`).join('');
|
||||
|
||||
function buildCard() {
|
||||
@@ -737,21 +736,11 @@ document.getElementById('prGo').addEventListener('click', () => {
|
||||
const boardFor = m => m === 'zombie' || m === 'cup' || m === 'train' ? stats.MODE_MATCH
|
||||
: m === 'classic' ? stats.MODE_CLASSIC : stats.MODE_GORE;
|
||||
|
||||
// what the setup footer says you are about to play, and what it is worth beating
|
||||
|
||||
// The splash carries ONE number, on the STATS button. A boxed BEST RUN / WORLD RANK bar across the
|
||||
// top came out of the mockup and went again: it sat on the best part of the art and said twice what
|
||||
// the two buttons underneath already say. Re-read whenever the splash comes back up, because a run
|
||||
// has just ended and the count has moved.
|
||||
// The splash keeps career progress on the plaque; detailed records live behind STATS and BOARD.
|
||||
// Re-read on each arrival because the preceding run may have moved the level or league.
|
||||
function refreshSplash() {
|
||||
// THE TEXT REDUCTION (2026-08-31): the two secondary doors carry a NUMBER each instead of a
|
||||
// description — what you have done, and your own best on the board they open. The board figure
|
||||
// is read locally on purpose: the splash does not fetch the world board.
|
||||
document.getElementById('mKills').textContent = stats.total().kills.toLocaleString('en-GB');
|
||||
document.getElementById('mBest').textContent =
|
||||
'BEST ' + stats.bestScore(boardFor(pickedMode)).toLocaleString('en-GB');
|
||||
// THE PLAQUE — the meta identity, re-read whenever the splash comes back because a run has just
|
||||
// moved it (same rule as the kill count above). One row now: crest, name and level, and the bar
|
||||
// moved it. One row now: crest, name and level, and the bar
|
||||
// under them wearing the level it fills toward. The goal LINE is gone — the fixture button below
|
||||
// says what to do, and it said it twice.
|
||||
const pr = profile.progress(), lg = leagueOf(profile.prof.lg);
|
||||
@@ -819,6 +808,9 @@ document.getElementById('mStart').addEventListener('click', () => {
|
||||
pickMode('cup'); start('cup');
|
||||
});
|
||||
document.getElementById('mOther').addEventListener('click', goMode);
|
||||
document.getElementById('mGore').addEventListener('click', () => {
|
||||
pickMode('gore'); start('gore');
|
||||
});
|
||||
document.getElementById('mStats').addEventListener('click', openStats);
|
||||
// the world board opens on the mode you last PICKED, like the stats screen does — that is the one
|
||||
// whose names you came to look at
|
||||
|
||||
Reference in new issue
Block a user