Make the opponent level matter in the cup
This commit is contained in:
1 parent
058636fae3
commit
bfc70d7388
8 files changed
+80
-19
No files matched your search
+8
-4
@@ -367,8 +367,8 @@ export function startMatch(cup = false, draw = null, train = 0) {
|
||||
if (G.interlude || G.horde || G.bossFight || G.drill || G.match || G.bench || G.net) return false;
|
||||
// THE LEAGUES: the profile's league is the world (band, pace tier, court — see PROJECT.md).
|
||||
// Stamped onto G so gameplay's leaguePace() and the dials below never read the profile module.
|
||||
// Netplay and training stay league-1 worlds: a human opponent and a practice rep both play the
|
||||
// shipped game (train still picks any met rung, but at the shipped pace).
|
||||
// Netplay stays a league-1 world. Training still stamps league 1 because it promotes nobody,
|
||||
// but its selected opponent supplies the real territory pace through gameplay.leaguePace.
|
||||
G.league = G.net || train ? 1 : prof.lg;
|
||||
// THE CLIMB: the arcade opens at the very BOTTOM of the ladder and runs the whole earned club,
|
||||
// clamping at your league's top (matchGameWon) — the marathon whose length the promotions buy.
|
||||
@@ -669,17 +669,21 @@ function matchOpponent() {
|
||||
const cup = G.match.cup;
|
||||
// THE WALK-ON wears its own zoned layout (render.drawFoeModal — Oliver: the generic line
|
||||
// stack was "a wall of text"). `foe` drives the drawing; title/body still carry the same
|
||||
// words for the suite and any legacy reader. The level pair is display only.
|
||||
// words for the suite and any legacy reader. The level pair is also the cup's readiness measure:
|
||||
// gameplay.peerK turns closing this gap into extra legs and timing, without weakening the body.
|
||||
const sub = cup ? [cup.rounds[cup.round].name + (cup.rounds[cup.round].need > 1
|
||||
? ' · FIRST TO ' + cup.rounds[cup.round].need + ' GAMES' : '')]
|
||||
// the top of the band walking BACK on is the arcade's natural "what now?" moment — the
|
||||
// intro answers it in place (THE LEAGUES: the cup is the only promotion)
|
||||
: G.match.again ? ['NOBODY STRONGER IS LEFT', 'THE CUP IS THE WAY UP'] : [];
|
||||
const lvl = foeLevel(G.match.opp), mine = levelAt(prof.xp);
|
||||
const gap = lvl - mine;
|
||||
const peer = gap > 5 ? gap + ' LEVELS ABOVE YOU' : gap > 0 ? 'ALMOST EVEN'
|
||||
: gap === 0 ? 'EVENLY MATCHED' : 'YOU HAVE THE EDGE';
|
||||
showModal({ title: zb.foe.name, color: UI.hot,
|
||||
body: [zb.foe.line, 'LEVEL ' + lvl + ' — YOURS: ' + mine].concat(sub),
|
||||
foe: { head: 'z_' + zb.fam + 'head', name: zb.foe.name, line: zb.foe.line,
|
||||
lvl, mine, sub } });
|
||||
lvl, mine, peer, sub } });
|
||||
// ...and a fresh body has read NOTHING yet: the habit tracker starts clean (see THE READ, config)
|
||||
G.match.laneRun = 0; G.match.lastLane = undefined; G.match.surprised = G.match.expected = false;
|
||||
// a FLAT pool per body: with regicide gone the body is pure scoreboard (matchPoint takes a
|
||||
|
||||
+8
-3
@@ -556,6 +556,10 @@ export const TUNE = {
|
||||
// ball pace on BOTH rackets and the opponent's legs together, so league 1
|
||||
// IS the shipped game (factor 1.0, the calibration rule) and league 5
|
||||
// plays ~24% faster. The one knob for "das Spiel wird schneller".
|
||||
peerGap: 15, // THE CUP'S SOFT WALL: this many levels below its opponent earns none of
|
||||
// the peer gift; reaching parity earns all of it. Never a penalty.
|
||||
peerRun: 0.08, // parity adds 8% legs on top of the permanent level bonus
|
||||
peerWindow: 0.12, // ...and 12% contact window. Skill still wins; grinding opens the margin
|
||||
matchSpeedBase: 1.25, // racketAI speed multiplier for opponent ONE. 1.4 was calibrated against
|
||||
// the broken recovery (the player never walked back to centre, so every
|
||||
// wide return looked like opponent strength) — with real footwork restored
|
||||
@@ -933,9 +937,10 @@ export const leagueOf = lg => LEAGUES[Math.min(LEAGUES.length, Math.max(1, lg |
|
||||
// which league a LADDER POSITION belongs to — THE CLIMB's tempo reads this (the world speeds up
|
||||
// as the run crosses each band border), and it is what makes the club read as five territories
|
||||
export const leagueOfRung = n => LEAGUES.find(l => n <= l.band[1]) || LEAGUES[LEAGUES.length - 1];
|
||||
// the DISPLAYED level of a ladder position (the walk-on card's "am I its peer yet?" line,
|
||||
// 2026-08-29): the ladder spread over the profile's 50 levels. Display only — nothing scales
|
||||
// off it. 49 = profile.LEVEL_CAP - 1 (config cannot import profile; the cycle runs the other way).
|
||||
// the level of a ladder position (the walk-on card's "am I its peer yet?" line, 2026-08-29):
|
||||
// the ladder spread over the profile's 50 levels. gameplay.peerK reads it for the cup/training
|
||||
// readiness gift; the opponent itself never scales with the player's level. 49 =
|
||||
// profile.LEVEL_CAP - 1 (config cannot import profile; the cycle runs the other way).
|
||||
export const foeLevel = n => Math.round(1 + (n - 1) / (LADDER - 1) * 49);
|
||||
|
||||
// The tournament, PER LEAGUE. Same shape CUP_ROUNDS always had (name/band/need) — round one is a
|
||||
|
||||
+9
-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, matchWound, LADDER, LEAGUES, leagueOfRung, TIN_LEAGUE, TIN_DOUBLE_LEAGUE } 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, LEAGUES, leagueOfRung, foeLevel, TIN_LEAGUE, TIN_DOUBLE_LEAGUE } 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';
|
||||
@@ -8,7 +8,7 @@ import { axisX, axisZ, takeFlickAim, touchPX, resetInput } from './input.js';
|
||||
import { W } from './render.js'; // runtime-only cycle (render imports gameplay), same pattern as input/zombies
|
||||
import { updateDrill } from './drill.js';
|
||||
import { endRun, beginRun, MODE_CLASSIC } from './stats.js';
|
||||
import { award, beginRunXp, prof, bonus } from './profile.js';
|
||||
import { award, beginRunXp, prof, bonus, levelAt } from './profile.js';
|
||||
import { updateZombies, updateParts, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, zombies, gore, explode, meleeSwipe, extras, parts, pellets, shots, playerGore, eatenBleed, resetZombies , firePellet , spawnBarrel , fireBeam , pops, popText } from './zombies.js';
|
||||
import { proceedAfterClear, startDuel, startHorde, warnHorde, resolveInterlude, watchdog, updateBench, benchKey, tickMatchFall, placeReceiver } from './beats.js'; // runtime-only cycle: beats calls back into cheer/aside/endBurst/deliverTin/showModal/logSwing
|
||||
import { netSendShot, netSendToss, netTick, netJudging, netSendPoint, netSendFault, netClose } from './net.js'; // runtime-only cycle: net calls back into pointTo/fault/tossBall/showModal
|
||||
@@ -1319,8 +1319,13 @@ export const rallyFeel = () => !!(G.match || G.classic);
|
||||
// with the level — LEAGUE modes only (the climb, the cup, the training court). Classic keeps its
|
||||
// own difficulty picker, netplay is a fair human match, gore is untouched (its trio lives in
|
||||
// profile.bonus too, read elsewhere). Level 1 is factor 1: the shipped baseline is the floor.
|
||||
export const legLevel = () => G.match && !G.net ? bonus().run : 1;
|
||||
export const winLevel = () => G.match && !G.net ? bonus().window : 1;
|
||||
// THE PEER GIFT turns the walk-on's displayed level into a real promise. It exists only in the
|
||||
// canonical cup and its exact training rep: arcade remains a stable whole-club challenge. Fifteen
|
||||
// levels down is today's wall, parity gets the full gift, and being underlevelled is never a malus.
|
||||
export const peerK = () => G.match && !G.net && (G.match.cup || G.match.train)
|
||||
? Math.max(0, Math.min(1, 1 - (foeLevel(G.match.opp) - levelAt(prof.xp)) / TUNE.peerGap)) : 0;
|
||||
export const legLevel = () => G.match && !G.net ? bonus().run * (1 + TUNE.peerRun * peerK()) : 1;
|
||||
export const winLevel = () => G.match && !G.net ? bonus().window * (1 + TUNE.peerWindow * peerK()) : 1;
|
||||
// legLevel rides INSIDE assistK, so the mover's legs and intercept's reachability can never
|
||||
// disagree about what those legs can get to (the rzChaseSpeed rule, player-side).
|
||||
export const assistK = () => (rallyFeel() ? Math.max(TUNE.autoAssist, TUNE.matchAssist) : TUNE.autoAssist)
|
||||
|
||||
+3
-3
@@ -1,15 +1,15 @@
|
||||
// profile.js — THE META-PROGRESSION PROFILE: xp, level, league, and its server mirror.
|
||||
//
|
||||
// The plan of record is PROJECT.md "THE LEAGUES". Phase 1 (this file): the clock ticks and is
|
||||
// visible; nothing reads the level back into gameplay yet. The profile is the ONE persistent
|
||||
// visible; gameplay reads its bounded bonuses back through bonus()/levelAt(). The profile is the ONE persistent
|
||||
// object the leagues hang off: xp/level (you), league + champion flag (the world), and the setup
|
||||
// flag that gates the first-boot identity screen. Name, tour, kit and skin deliberately KEEP
|
||||
// their existing homes (stats' NAME_KEY, the picker localStorage keys) — this file only mirrors
|
||||
// them to the server so a wiped phone gets its whole identity back, not just its number.
|
||||
//
|
||||
// XP comes from ACTIVE PLAY EVENTS, never wall-clock (settled): losing pays too, training pays
|
||||
// nothing. award() carries the no-stakes guard itself — drill, bench, test mode, the training
|
||||
// court — so a new call site cannot forget it.
|
||||
// one quarter. award() carries the no-stakes guard itself — drill, bench and test mode — so a new
|
||||
// call site cannot forget it.
|
||||
//
|
||||
// The server mirror is a BACKUP, not an account: localStorage is the authority while it exists,
|
||||
// and pull() adopts the mirror only when it is AHEAD on xp (a wiped phone, a second device).
|
||||
|
||||
+5
-1
@@ -2368,7 +2368,7 @@ function modalRows(m, inner, skipBody) {
|
||||
const rows = [];
|
||||
const push = r => { rows.push(r); return r; };
|
||||
if (m.foe) {
|
||||
push({ kind: 'split', h: 26 });
|
||||
push({ kind: 'split', h: m.foe.peer ? 38 : 26 });
|
||||
const sub = [];
|
||||
for (const l of m.foe.sub || []) sub.push(...wrapText(l, F(8, 0), inner));
|
||||
if (sub.length) push({ kind: 'text', lines: sub, dim: true, h: sub.length * 12 + 2 });
|
||||
@@ -2494,6 +2494,10 @@ function drawModal(m) {
|
||||
shadowText('LEVEL ' + f.lvl, inX, y + 9, f.lvl > f.mine ? col : UI.pick);
|
||||
ctx.textAlign = 'right';
|
||||
shadowText('YOURS ' + f.mine, x + L.boxW - pad, y + 9, UI.dim);
|
||||
if (f.peer) {
|
||||
ctx.font = F(8, 700); ctx.textAlign = 'left';
|
||||
shadowText(f.peer, inX, y + 25, f.lvl > f.mine ? col : UI.pick);
|
||||
}
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
y += r.h + MODAL_GAP;
|
||||
|
||||
Reference in new issue
Block a user